//@version=6
// © IlSeparatio369.
// SWING ATLAS 369 | Multi-period Structure, Volume & Signal Review
// Release candidate 1.0 
//
// PURPOSE AND CONTRIBUTION
// Swing Atlas 369 is IlSeparatio369's integrated chart-analysis adaptation:
// confirmed-close reversal intervals, candle-based swing/calendar volume profiles,
// D/W/M body Fibonacci and range scenarios, bounded reference clusters, session
// context, frozen signal snapshots and a conservative first-touch study.
// The design combines these modules around one question: what information was
// actually available when the signal confirmed? It is an INDICATOR, not a broker
// terminal, automated strategy, fill tracker or guarantee of future performance.
//
// START HERE
// 01 Workspace: Minimal is the default; Analysis opens the research dashboards.
// 02 Signals and costs: set actual estimated costs; defaults are zero. Larger
// swing size normally tolerates wider pullbacks but also delays reversals.
// 03 Volume: distinguish swing profiles from independent D/W/M distributions.
// 04 Reference map: body/range scenarios, focus radius and compact price labels.
// 05 Session and context: custom-session VWAP/levels and confirmed Supertrend.
// 06 Research: modeled intervals, frozen snapshots and reaction breakdowns.
// 07 Appearance: chart colors, panels, markers and retained visual history.
// 08 Advanced tuning: explicit smoother, ATR and resource controls.
// Input values are omitted from the status line/Data Window. Pine v5 controls
// remain accessible in Inputs; grouping does not conceal the source or methods.
// Use standard candles. 3/6/15m are use cases, not optimized/proven settings.
//
// SIGNALS VERSUS HISTORICAL GEOMETRY
// A Jurik-style smoother and ratcheting ATR threshold generate alternating
// BUY/SELL states at candle close. A strong reversal can bypass the slope check,
// never the close gate. Initial signal establishes direction; later signals
// close the modeled opposite interval and begin the new interval.
// ZigZag endpoints are drawn at earlier extremes AFTER confirmation. HIGH/LOW
// labels measure entry-close to exit-close intervals, excluding earlier wicks
// of the entry candle. Neither historical extremes nor signal prices are fills.
// POC/Fib/Supertrend/session modules provide context, not extra trade triggers.
//
// VOLUME METHODS AND AVAILABILITY
// POC = midpoint of the maximum-volume row; exact ties choose the lowest row.
// Candle volume is allocated uniformly by high-low overlap: an estimate, not
// tick volume-at-price, order flow, bid/ask delta or built-in profile parity.
// Swing profiles include full candles at BOTH endpoints, sharing boundary
// candles with neighboring profiles. Freeze occurs on reversal confirmation;
// the forward extension starts there. Partial intrabars use chart fallback.
// Missing required volume or overwritten samples skip the profile; the prior
// valid swing POC may remain, explicitly marked RETAINED with availability age.
// Calendar profiles independently aggregate D/W/M using a 60m source by default
// and 6000 requested source bars. First partial periods, invalid samples and
// capacity overflow are excluded. A new POC releases after the first source
// candle of the next period closes, mapped at the next source open. Gaps can
// delay publication. Standard chart timeframe must be <= calendar source.
// Developing POCs use closed source candles; they move and reset each period.
//
// REFERENCE DEFINITIONS
// Prior completed O/H/L/C define each daily, weekly or monthly set:
// body retracements C-r*(C-O), r=.382/.5/.618;
// body extensions O+e*(C-O), e=1.272/1.618/2;
// range scenarios current-period O +/- prior (H-L)*.618/1/1.618.
// Signed bodies set direction; tiny bodies are suppressed. These are not ABC
// extensions or automatic take-profit orders. Prior OHLC/ATR uses [1] with
// lookahead_on; current opens are fixed at the period start. No unfinished
// higher-timeframe close is used. Boundaries follow the symbol/feed.
// Compact view selects two distinct Fib prices per side of its anchor. Focus
// can hide distant calendar references; Show all bypasses it. Merged tags list
// exact component prices. Clusters require two distinct families; ratios in
// one family count once. Proximity is not independence or success probability.
//
// CONTEXT AND EVIDENCE
// Supertrend is latched at chart close. ALIGNED/MIXED never vetoes a signal.
// Custom session VWAP = sum(HLC3*volume)/sum(volume) from closed chart candles.
// First/unaligned sessions are excluded. Prior H/L publishes at the next closed
// outside/new-session candle. Named timezones handle overnight/DST boundaries.
// Snapshots copy confirmation-close scalar data, including nearby enabled Fib
// references and quality flags. They are bounded and reconstruct on reload.
// First-touch study follows swing POC, D/W/M POC and D/W/M body .618 retracement.
// Prior close AND open must be strictly on one side; gap-through returns and
// opens exactly on the level are excluded. Freeze ATR distances at touch close,
// then evaluate only the next 1..N candles. Both thresholds in one candle means
// AMBIGUOUS; no threshold by deadline means EXPIRED; bad OHLC is separate.
// Breakdown filters select source/approach within the global rolling resolved
// sample. Overlapping observations are not independent trades or probabilities.
// Presets change visuals only. Research controls never change the signal rules.
//
// COSTS, ALERTS AND LIMITS
// Modeled net return uses signal closes minus 2*fee + spread + 2*slippage.
// No actual fills, position sizing, leverage, compounding, funding or liquidation
// model. Cost filtering concerns reversal distance, not guaranteed trade profit.
// One Any alert() function call alert per execution stream; recreate after edits.
// Generic JSON is a bridge contract, NOT a native PrimeXBT request. A bridge
// must validate account/symbol/size, deduplicate, reconcile, close the opposite
// position, verify its fill, then open the target. Failed close blocks opening.
// Historical signals do not initialize accounts. Broker execution is UNVERIFIED.
// Do not combine conflicting timeframe streams without explicit arbitration.
// No protective broker stop is provided. Continuous reversal is not risk control.
// No indicator can identify every future top/bottom. Wider swings can increase
// delay and adverse movement. Developing visuals move; feed/history revisions
// or input changes can reconstruct results. Local checks are not Pine Editor
// compilation, runtime profiling, visual QA or proof of profitable trading.
// Audit Data Window plots expose confirmed event timestamps and direction for
// manual live/reload comparisons; they do not persist an external execution log.
//
// ATTRIBUTION AND SCOPE OF AUTHORSHIP
// Original Zero Lag ZigZag concept: © Joris Duyck (JD), 2019 to present.
// Jurik-style filter implementation credited to Everget, as in JD's source.
// Adaptation, profile/MTF/evidence modules and presentation: © IlSeparatio369.
// Educational chart analysis; no guaranteed trading outcomes.

indicator('Swing Atlas 369 | MTF Volume & Fib', shorttitle = 'Atlas369', overlay = true, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 300)
gStart = '01 - Workspace'
viewPreset = input.string('Minimal', 'Presentation preset', options = ['Minimal', 'Analysis', 'Publication'], group = gStart, display = display.none, tooltip = 'Analysis shows enabled research panels. Minimal keeps a compact status card and hides profile histograms, map text and session H/L/open plots. Publication hides dashboards but retains chart annotations. All three use identical calculations, snapshots, reaction rules and BUY/SELL signals.')
distance = input.float(1.6, 'Swing size (ATR)', minval = 0.2, step = 0.1, group = gStart, display = display.none, tooltip = 'Higher = wider swings and usually longer holds. Lower = faster reversals and more noise. Not a broker stop.')
showSignals = input.bool(true, 'BUY / SELL labels with close price', group = gStart, display = display.none, tooltip = 'Shows confirmed signal markers. A leader connects each offset label to the actual confirmation-close dot.')
showPOC = input.bool(true, 'Confirmed swing POC', group = gStart, display = display.none, tooltip = 'Gold POC appears only when a signal-derived swing confirms. Its historical segment is retrospective; the live extension starts at confirmation. Independent of trade signals.')
showCalendarPOC = input.bool(true, 'Calendar POC levels', group = gStart, display = display.none, tooltip = 'Independent completed day/week/month distributions, never averages of lower-period POCs. Uses a separate source timeframe so short chart history need not prevent monthly coverage. Source-feed history must still contain a full period. First partial period is discarded.')
showDaily = input.bool(true, 'Daily analytical levels', group = gStart, inline = 'moduleDW', display = display.none, tooltip = 'Current day only, on chart timeframes up to 1D. Uses the symbol\'s daily-feed boundaries, not your display timezone. Never supplies order exits.')
showWeekly = input.bool(true, 'Weekly analytical levels', group = gStart, inline = 'moduleDW', display = display.none, tooltip = 'Uses the previous completed weekly candle and this week\'s open. Symbol/feed week boundaries; disabled above the weekly timeframe. Analytical references only.')
showMonthly = input.bool(true, 'Monthly analytical levels', group = gStart, inline = 'moduleDW', display = display.none, tooltip = 'Uses the previous completed monthly candle and this month\'s open. Calendar months follow the symbol/feed. Disabled above the monthly timeframe.')
showSession = input.bool(true, 'Calculate custom session context', group = gStart, display = display.none, tooltip = 'Session open, previous completed session high/low and closed-candle HLC3-volume VWAP. Standard intraday charts only. Requires aligned chart candles and observed full session boundaries; partial first sessions are rejected. Does not filter trades.')
showST = input.bool(true, 'Supertrend context line', group = gStart, display = display.none, tooltip = 'TradingView built-in Supertrend, latched on chart-candle close. Directional context only: never changes the existing reversal rules or creates an order.')
sendAlerts = input.bool(true, 'Enable reversal alerts', group = gStart, display = display.none, tooltip = 'Enable dynamic alert() messages. Create an Any alert() function call alert; existing TradingView alerts must be recreated after edits.')

gSignal = '02 - Signals and costs'
costFilter = input.bool(true, 'Require reversal movement beyond modeled costs', group = gSignal, display = display.none, tooltip = 'Applies to the pullback from the swing extreme, not to profit since entry. Can delay reversals. Zero cost inputs mean no additional filtering.')
feePct = input.float(0.0, 'Commission per side (%)', minval = 0, maxval = 5, step = 0.001, group = gSignal, inline = 'costs', display = display.none, tooltip = 'Enter your actual fee as percent of notional: 0.05 means 0.05%. No broker fee is assumed.')
spreadPct = input.float(0.0, 'Full spread (%)', minval = 0, maxval = 5, step = 0.001, group = gSignal, inline = 'costs', display = display.none, tooltip = 'Approximate full bid-ask spread expressed as percent of price, deducted once per modeled round trip.')
slipPct = input.float(0.0, 'Slippage per side (%)', minval = 0, maxval = 5, step = 0.001, group = gSignal, inline = 'costs', display = display.none, tooltip = 'Approximate slippage percent for each entry and exit. Actual fills can be worse.')
costBuffer = input.float(1.5, 'Cost floor multiplier', minval = 1, maxval = 3, step = 0.1, group = gSignal, display = display.none, tooltip = 'Only scales the reversal filter. Statistics deduct the unscaled cost estimate.')
jsonAlerts = input.bool(false, 'Generic bridge JSON (not PrimeXBT API format)', group = gSignal, display = display.none, tooltip = 'For a separately implemented execution bridge. This schema cannot be sent directly as a native PrimeXBT order.')

gVolume = '03 - Volume profiles'
showProfile = input.bool(true, 'Histogram inside completed swing', group = gVolume, display = display.none, tooltip = 'Uses full endpoint candles, including their entire volume. Adjacent profiles share the endpoint candle. Width indicates relative row volume, not elapsed trading time.')
profileTF = input.timeframe('1', 'Lower timeframe source', group = gVolume, display = display.none, tooltip = 'Choose below the chart timeframe. Missing, incomplete or non-lower requests use a chart-candle fallback. Provider volume is not broker volume.')
profileRows = input.int(32, 'Price rows', minval = 8, maxval = 64, group = gVolume, display = display.none, tooltip = 'POC is the midpoint of the maximum-volume row. More rows mean finer bins, not greater source-data accuracy.')
profileKeep = input.int(2, 'Completed profiles to retain', minval = 1, maxval = 4, group = gVolume, display = display.none, tooltip = 'Retains this many completed profiles; only the latest valid POC is extended right. This does not change signal history.')
calSource = input.timeframe('60', 'Calendar sampling timeframe', options = ['15', '30', '60'], group = gVolume, display = display.none, tooltip = '60m default gives roughly 250 calendar days of crypto coverage with 6000 requested source bars. 15m gives about 62 days and finer estimates. Actual provider history may be shorter. Use a chart timeframe at or below this source; other chart timeframes disable calendar POCs. Coarse source candles cannot reconstruct actual volume-at-price.')
calRows = input.int(48, 'Calendar price rows', minval = 8, maxval = 100, group = gVolume, display = display.none, tooltip = 'Uniform candle-volume overlap over the full observed period range. POC is the maximum row midpoint; exact ties choose the lowest row. An estimate, not built-in volume-profile parity.')
calDEnabled = input.bool(true, 'Previous day POC', group = gVolume, inline = 'cal', display = display.none, tooltip = 'Completed daily volume distribution from source candles, using symbol session boundaries.')
calWEnabled = input.bool(true, 'Previous week POC', group = gVolume, inline = 'cal', display = display.none, tooltip = 'Independent completed weekly distribution; not an average of daily POCs.')
calMEnabled = input.bool(true, 'Previous month POC', group = gVolume, inline = 'cal', display = display.none, tooltip = 'Independent completed monthly distribution. Missing full-month coverage is shown as unavailable, never extrapolated.')
showDevelopingPOC = input.bool(false, 'Developing calendar POCs', group = gVolume, display = display.none, tooltip = 'Dashed provisional profiles use closed source candles only. They are hidden at a new period until its first closed source sample arrives, change as the period develops, are delayed by the source interval, and are excluded from confluence. Enabling them increases computation.')

gReference = '04 - Reference map'
showDailyOpen = input.bool(true, 'Today\'s open', group = gReference, display = display.none, tooltip = 'Displays the current daily-feed open. It is fixed from the day\'s start, unlike an unfinished daily close.')
showBodyRetrace = input.bool(true, 'Previous-day body retracements', group = gReference, inline = 'daybody', display = display.none, tooltip = 'C-r*(C-O), r=0.382/0.5/0.618, using yesterday\'s confirmed open/close.')
showBodyExtend = input.bool(true, 'Previous-day body extensions', group = gReference, inline = 'daybody', display = display.none, tooltip = 'O+e*(C-O), e=1.272/1.618/2.0. Extensions continue yesterday\'s body direction; they are not ABC swing extensions.')
showRangeTargets = input.bool(true, 'Daily-range projections: both directions', group = gReference, display = display.none, tooltip = 'Today\'s open +/- yesterday\'s full high-low range * 0.618/1/1.618. Hypothetical distances, not predicted prices.')
showWeeklyOpen = input.bool(true, 'This week\'s open', group = gReference, display = display.none, tooltip = 'Fixed opening price of the current exchange/feed week. Does not use the unfinished weekly close.')
showWeeklyRetrace = input.bool(true, 'Previous-week body retracements', group = gReference, inline = 'weekbody', display = display.none, tooltip = 'Previous weekly C - ratio*(C-O), ratios .382/.5/.618. Signed body determines direction; small bodies are suppressed.')
showWeeklyExtend = input.bool(true, 'Previous-week body extensions', group = gReference, inline = 'weekbody', display = display.none, tooltip = 'Previous weekly O + ratio*(C-O), ratios 1.272/1.618/2. No three-pivot ABC pattern or predicted outcome is implied.')
showWeeklyRange = input.bool(true, 'Weekly-range projections: both directions', group = gReference, display = display.none, tooltip = 'This week\'s open +/- previous completed week\'s high-low range * .618/1/1.618. Fixed hypothetical distances, independent of body direction.')
showMonthlyOpen = input.bool(true, 'This month\'s open', group = gReference, display = display.none, tooltip = 'Fixed opening price of the current monthly feed candle. Does not use an unfinished monthly close.')
showMonthlyRetrace = input.bool(true, 'Previous-month body retracements', group = gReference, inline = 'monthbody', display = display.none, tooltip = 'Previous monthly C - ratio*(C-O), ratios .382/.5/.618. Suppresses small bodies using the shared minimum body/range threshold.')
showMonthlyExtend = input.bool(true, 'Previous-month body extensions', group = gReference, inline = 'monthbody', display = display.none, tooltip = 'Previous monthly O + ratio*(C-O), ratios 1.272/1.618/2. Analytical extensions, never automatic take-profit orders.')
showMonthlyRange = input.bool(true, 'Monthly-range projections: both directions', group = gReference, display = display.none, tooltip = 'This month\'s open +/- previous completed month\'s high-low range * .618/1/1.618. Can lie far from current price; compact display hides distant Fib levels.')
minBodyRatio = input.float(0.05, 'Minimum body / period range', minval = 0, maxval = 0.5, step = 0.01, group = gReference, display = display.none, tooltip = 'Suppresses body Fib levels on doji/small-body days; does not change range projections. A zero body is always suppressed.')
showConfluence = input.bool(true, 'Highlight fixed-reference clusters', group = gReference, display = display.none, tooltip = 'Clusters enabled fixed D/W/M body, range and POC families plus the latest valid swing POC. Multiple ratios from one family count once. Opens/developing POCs excluded. Geometric proximity, not probability.')
confluenceATR = input.float(0.1, 'Maximum distance / daily ATR', minval = 0.01, maxval = 0.5, step = 0.01, group = gReference, display = display.none, tooltip = 'Uses yesterday\'s confirmed daily ATR(14). Cluster membership uses fixed reference prices. Nearest displayed clusters follow the selected live/last-close anchor and visual focus radius.')
mapShowAll = input.bool(false, 'Show all enabled Fibonacci levels', group = gReference, display = display.none, tooltip = 'Off: shows the nearest two distinct Fib prices above and below price across D/W/M. Enabled opens and POCs stay visible within the optional focus radius. On: draws all enabled levels and bypasses focus. Calculations do not change.')
mapSelection = input.string('Last closed price', 'Reference selection anchor', options = ['Last closed price', 'Live price'], group = gReference, display = display.none, tooltip = 'Last closed price stabilizes nearest-level selection during an unfinished chart candle. Live price follows every tick. This changes only map selection and dashboard distances, never signal logic or underlying reference prices.')
mapFocus = input.bool(true, 'Focus nearby calendar references', group = gReference, display = display.none, tooltip = 'In compact mode, hides calendar/Fibonacci lines, labels and clusters outside the selected daily-ATR distance. Calculations and nearest-reference dashboard remain available. The latest swing POC is retained. Show all levels overrides this visual filter.')
mapFocusATR = input.float(3.0, 'Focus radius / confirmed daily ATR', minval = 0.25, maxval = 20, step = 0.25, group = gReference, display = display.none, tooltip = 'Visual radius around the selection anchor, using previous completed daily ATR(14). Missing ATR disables this filter. This is not a volatility forecast or trade filter.')
showDailyLabels = input.bool(true, 'All reference-map labels', group = gReference, display = display.none, tooltip = 'Shows merged labels for D/W/M references and swing POC. Hover for exact component prices; turn off to reduce text.')
dailyLabelOffset = input.int(28, 'Reference labels: bars to the right', minval = 1, maxval = 150, group = gReference, display = display.none, tooltip = 'Moves reference labels into the right margin; does not change availability or calculation prices.')
mapMergeATR = input.float(0.025, 'Merge label spacing / confirmed daily ATR', minval = 0, maxval = 0.25, step = 0.005, group = gReference, display = display.none, tooltip = 'Nearby label names are grouped to reduce overlap; original horizontal lines and exact prices remain unchanged and every price is listed in the tooltip. Group span is bounded, never chained across a wide range.')
mapMaxZones = input.int(3, 'Maximum confluence zones', minval = 0, maxval = 6, group = gReference, display = display.none, tooltip = 'Draws nearest qualifying fixed-reference clusters. A cluster needs two distinct source families, e.g. D Body + W POC. Multiple ratios of one family count once. Opens and developing POCs are excluded; geometric proximity is not probability.')

gContext = '05 - Session and context'
sessionHours = input.session('0000-0000', 'Session hours', group = gContext, display = display.none, tooltip = 'One continuous session. Equal start/end means 24 hours. Overnight intervals retain the same identity across midnight. Match your chart bar alignment to both session boundaries.')
sessionDays = input.string('1234567', 'Session days', options = ['1234567', '23456'], group = gContext, display = display.none, tooltip = 'TradingView session day notation: 1 Sunday through 7 Saturday. All days suits continuous markets; weekdays uses 23456. Overnight day membership follows TradingView session semantics.')
sessionZone = input.string('Etc/UTC', 'Session timezone', options = ['Etc/UTC', 'America/New_York', 'Europe/London', 'Europe/Bucharest', 'Asia/Tokyo', 'Exchange'], group = gContext, display = display.none, tooltip = 'Named IANA zones follow daylight-saving rules. Exchange uses the symbol timezone. This is independent of the chart display timezone.')
showSessionVWAP = input.bool(true, 'Session VWAP line', group = gContext, display = display.none, tooltip = 'Sum(HLC3*volume)/sum(volume) from full closed chart candles. Missing/negative volume invalidates the current session VWAP. Zero cumulative volume shows no value. Developing session VWAP can change after each candle close.')
showSessionLevels = input.bool(true, 'Session open and prior high/low', group = gContext, display = display.none, tooltip = 'Only fully observed sessions can produce prior-session high/low. Missing volume disables VWAP but does not by itself invalidate OHLC levels. Minimal preset hides these three plots.')
stLength = input.int(10, 'ATR length', minval = 1, maxval = 200, group = gContext, display = display.none, tooltip = 'ATR lookback for Supertrend context. Independent of the ZigZag reversal ATR.')
stFactor = input.float(3.0, 'ATR multiplier', minval = 0.1, maxval = 20, step = 0.1, group = gContext, display = display.none, tooltip = 'Larger values normally keep Supertrend farther from price. Agreement with ZigZag is context, not independent confirmation or a success probability.')
earlyWarning = input.bool(true, 'Show provisional reversal warning', group = gContext, display = display.none, tooltip = 'Provisional intrabar preview; can disappear. Does not trigger execution alerts.')
warningATR = input.float(0.25, 'Warning proximity (ATR)', minval = 0, maxval = 2, step = 0.05, group = gContext, display = display.none, tooltip = 'Amber preview near the reversal level when slope agrees. Intrabar only; can disappear. Never sends an execution alert.')
chopThreshold = input.float(0.25, 'Sideways: efficiency below', minval = 0.01, maxval = 0.8, step = 0.01, group = gContext, display = display.none, tooltip = 'Efficiency below this value is classified as sideways. Informational; does not block reversals.')
shadeChop = input.bool(false, 'Full-height sideways shading (optional)', group = gContext, display = display.none, tooltip = 'Optional full-height background shading. Off by default to keep the chart clean; dashboard warnings remain available.')

gEvidence = '06 - Research'
showQuality = input.bool(true, 'Data-quality panel', group = gEvidence, display = display.none, tooltip = 'Shows loaded-history chart-candle fallback and missing-volume counts, ring-buffer sample count, calendar source and POC availability. Counts are data diagnostics, not accuracy scores.')
showSnapshots = input.bool(true, 'Capture confirmed signal snapshots', group = gEvidence, display = display.none, tooltip = 'Copies values known on the BUY/SELL confirmation close: swing/calendar POCs, nearest enabled Fib above/below, Supertrend, session VWAP and quality flags. This is an audit record, not a hindsight target or broker fill.')
snapshotKeep = input.int(50, 'Snapshots retained', minval = 1, maxval = 100, group = gEvidence, display = display.none, tooltip = 'Bounded records from loaded chart history. Values freeze at each signal close. Reloading changed source data or inputs reconstructs the history; this is not permanent external storage.')
snapshotBack = input.int(0, 'Inspect snapshot: 0 = newest', minval = 0, maxval = 99, group = gEvidence, display = display.none, tooltip = 'Select an older retained snapshot in the research panel and Data Window. An unavailable index shows no record instead of substituting another signal.')
showReactions = input.bool(true, 'Track first eligible reference touches', group = gEvidence, display = display.none, tooltip = 'Tracks seven enabled reference streams: swing POC, D/W/M POC and D/W/M body retracement .618. One first eligible range-overlap touch per source-period/level identity. Counts overlapping observations, not independent trades or predictive probabilities.')
reactionSourceView = input.string('All', 'Reaction source', options = ['All', 'Swing POC', 'D POC', 'W POC', 'M POC', 'D RET .618', 'W RET .618', 'M RET .618'], group = gEvidence, display = display.none, tooltip = 'Selects a source subset of the same global rolling resolved window. It does not collect an independent N observations per source or change first-touch rules.')
reactionApproachView = input.string('Both', 'Reaction approach', options = ['Both', 'From above', 'From below'], group = gEvidence, display = display.none, tooltip = 'From above studies upward support reactions; From below studies downward resistance reactions. Counts remain overlapping observations, not trading win probabilities.')
reactionBars = input.int(12, 'Reaction evaluation bars after touch', minval = 1, maxval = 100, group = gEvidence, display = display.none, tooltip = 'Evaluate the NEXT 1..N closed chart candles; the touch candle is excluded because its extremes may precede contact. Existing observations retain their frozen level and thresholds even when reference sets change.')
reactionMoveATR = input.float(0.5, 'Reaction distance / chart ATR', minval = 0.05, maxval = 5, step = 0.05, group = gEvidence, inline = 'reactionDistance', display = display.none, tooltip = 'Freeze chart ATR at the touch candle close. Approach from above tests an upward reaction; approach from below tests a downward reaction. Requires previous close AND touch-bar open strictly on the same side. Gap-through returns and opening exactly at the level are excluded. Gaps without range overlap are not touches.')
reactionFailATR = input.float(0.5, 'Failure distance / chart ATR', minval = 0.05, maxval = 5, step = 0.05, group = gEvidence, inline = 'reactionDistance', display = display.none, tooltip = 'Freeze adverse distance at the touch candle close. If both reaction and failure thresholds occur within the same later candle, record AMBIGUOUS. No intrabar event order is assumed.')
reactionKeep = input.int(200, 'Resolved observations retained', minval = 20, maxval = 1000, group = gEvidence, display = display.none, tooltip = 'Rolling last N resolved observations in resolution order. R = reaction, F = failure, A = ambiguous, E = expired, X = invalid OHLC. Pending observations are separate. These counts do not include fees or represent trading returns.')
showStats = input.bool(true, 'Show statistics panel', group = gEvidence, display = display.none, tooltip = 'Displays modeled, unleveraged signal-trade statistics. Never verifies profitability or broker fills.')
statsWindow = input.int(200, 'Most recent completed trades', minval = 10, maxval = 1000, group = gEvidence, display = display.none, tooltip = 'Rolling sample of completed signal trades from loaded chart history, not a forward-tested broker record.')

gVisual = '07 - Appearance'
signalHistory = input.int(8, 'Recent trades with BUY / SELL text', minval = 1, maxval = 100, group = gVisual, display = display.none, tooltip = 'Limits BUY/SELL text only. Historical calculations and statistics are retained.')
showWarningLabel = input.bool(false, 'Floating early-warning label', group = gVisual, display = display.none, tooltip = 'Warning remains visible in the dashboard when this is off.')
historyFade = input.int(65, 'Historical swing transparency', minval = 0, maxval = 95, group = gVisual, display = display.none, tooltip = 'Transparency of older swing drawings. Higher values reduce historical clutter without changing calculations.')
focusTrades = input.int(2, 'Highlighted recent trades', minval = 1, maxval = 10, group = gVisual, display = display.none, tooltip = 'Number of recent trades kept bright. Older visual objects are faded according to historical transparency.')
showLiveText = input.bool(false, 'Show developing-swing price text', group = gVisual, display = display.none, tooltip = 'Optional text at the unconfirmed developing endpoint. Can change on every realtime update.')
cardPosition = input.string('Top right', 'Dashboard position', options = ['Top right', 'Bottom right', 'Top left', 'Bottom left'], group = gVisual, display = display.none, tooltip = 'Choose a chart corner for the dashboard. Visual placement only.')
palette = input.string('Emerald / Ruby', 'Palette', options = ['Emerald / Ruby', 'Ice / Coral', 'Custom'], group = gVisual, display = display.none, tooltip = 'Choose directional colors; Custom uses your Long/Short color inputs.')
uiTheme = input.string('Dark', 'Panel theme', options = ['Dark', 'Light'], group = gVisual, display = display.none, tooltip = 'Panel contrast for a dark or light chart background. Does not change the chart background itself.')
extremeHistory = input.int(2, 'Recent trades with HIGH / LOW text', minval = 1, maxval = 20, group = gVisual, display = display.none, tooltip = 'Older ZigZag endpoints remain visible; this limits price text to reduce clutter.')
markerGap = input.float(1.0, 'Signal label spacing (ATR)', minval = 0.1, maxval = 3, step = 0.1, group = gVisual, display = display.none, tooltip = 'Moves signal text away from candles; the leader/dot still marks the exact signal-close reference.')
showSignalPrice = input.bool(false, 'Print price inside BUY / SELL labels', group = gVisual, display = display.none, tooltip = 'Exact confirmation price always remains in the tooltip and on the signal dot.')
showZone = input.bool(false, 'Subtle directional zone', group = gVisual, display = display.none, tooltip = 'Optional translucent visual fill. Does not add a filter or an order condition.')
zigWidth = input.int(2, 'ZigZag width', minval = 1, maxval = 5, group = gVisual, display = display.none, tooltip = 'Width of the primary confirmed ZigZag line; the optional halo is wider and translucent.')
history = input.int(50, 'Completed trades to display', minval = 5, maxval = 100, group = gVisual, display = display.none, tooltip = 'Maximum visual history; has no effect on the rolling statistics window.')
showExtremes = input.bool(true, 'Exact HIGH / LOW of each signal trade', group = gVisual, display = display.none, tooltip = 'Measured from entry-signal close through exit-signal close. Entry candle\'s earlier wicks are excluded. Labels appear only after exit confirmation; these are not broker fills.')
showLevels = input.bool(true, 'Latest entry and confirmation levels', group = gVisual, display = display.none, tooltip = 'Latest signal entry-close and effective reversal threshold. These are not orders resting at a broker.')
showTradePath = input.bool(false, 'Dotted entry-to-exit connectors', group = gVisual, display = display.none, tooltip = 'Connects consecutive signal closes. Do not confuse these with the retrospective ZigZag extremes.')
showGlow = input.bool(true, 'Soft ZigZag halo', group = gVisual, display = display.none, tooltip = 'Adds a soft, transparent halo to swing segments. Turn off for a thinner technical drawing.')
showJurik = input.bool(false, 'Jurik ribbon', group = gVisual, display = display.none, tooltip = 'Displays the underlying smoother; visual toggle only, the filter continues calculating when hidden.')
labelOffset = input.int(8, 'Right-side label offset', minval = 1, maxval = 100, group = gVisual, display = display.none, tooltip = 'Horizontal offset for the latest entry, reversal and POC price tags. No lookahead is used in signal logic.')
showZZ = input.bool(true, 'Confirmed swing segments', group = gVisual, display = display.none, tooltip = 'Confirmed historical geometry appears after a reversal. Endpoints are retrospective, not executable hindsight trades.')
showLive = input.bool(true, 'Developing swing (can move)', group = gVisual, display = display.none, tooltip = 'Dotted developing geometry can move until confirmed. It never backdates a BUY or SELL.')
showTrail = input.bool(true, 'Reversal threshold', group = gVisual, display = display.none, tooltip = 'Displays the effective reversal threshold including the configured cost floor. Crossing still requires closed-bar confirmation.')
paintBars = input.bool(false, 'Color candles', group = gVisual, display = display.none, tooltip = 'Colors candles by the confirmed logical signal state; does not replace your chart\'s price data.')
showPanel = input.bool(true, 'Status panel', group = gVisual, display = display.none, tooltip = 'Displays state, cost assumptions, POC status and daily-level availability. Broker fills remain unverified.')
customUp = input.color(color.rgb(0, 220, 160), 'Long', group = gVisual, display = display.none, tooltip = 'Used for long/upward drawings only when Palette is Custom.')
customDown = input.color(color.rgb(255, 75, 100), 'Short', group = gVisual, display = display.none, tooltip = 'Used for short/downward drawings only when Palette is Custom.')
profileWidth = input.float(0.3, 'Histogram width / swing duration', minval = 0.1, maxval = 0.7, step = 0.05, group = gVisual, display = display.none, tooltip = 'Fraction of the completed swing\'s time span occupied by the largest volume row. Purely visual.')
profileOpacity = input.int(88, 'Histogram transparency', minval = 50, maxval = 98, group = gVisual, display = display.none, tooltip = 'Higher values make histogram rows fainter; the maximum-volume POC row remains emphasized.')
researchPosition = input.string('Bottom left', 'Research panel position', options = ['Top left', 'Bottom left', 'Top right', 'Bottom right'], group = gVisual, display = display.none, tooltip = 'Choose a different corner from the main status panel. Visible only in Analysis preset; calculations continue in all presets.')

gAdvanced = '08 - Advanced tuning'
src = input.source(ohlc4, 'Source', group = gAdvanced, display = display.none, tooltip = 'Price series for the Jurik-style filter. Source choice changes reversal timing.')
length = input.int(10, 'Smoother length', minval = 2, maxval = 200, group = gAdvanced, inline = 'shape', display = display.none, tooltip = 'Longer smooths more and usually delays turns; shorter reacts faster with more noise.')
phase = input.int(50, 'Phase', minval = -100, maxval = 100, group = gAdvanced, inline = 'shape', display = display.none, tooltip = 'Adjusts filter responsiveness/overshoot. This does not remove delay or make turning points predictable.')
power = input.float(2.0, 'Power', minval = 0.5, maxval = 5, step = 0.25, group = gAdvanced, inline = 'shape', display = display.none, tooltip = 'Controls the Jurik-style filter response. Keep fixed while evaluating other parameters.')
atrLen = input.int(14, 'ATR length', minval = 2, group = gAdvanced, display = display.none, tooltip = 'Volatility window for the reversal threshold; not a stop-loss order.')
adaptive = input.bool(true, 'Widen threshold in efficient trends', group = gAdvanced, display = display.none, tooltip = 'Widens the threshold in more efficient trends to tolerate pullbacks.')
erLen = input.int(10, 'Trend efficiency length', minval = 2, maxval = 100, group = gAdvanced, display = display.none, tooltip = 'Window used to measure net price movement relative to total movement.')
expansion = input.float(0.5, 'Maximum trend expansion', minval = 0, maxval = 2, step = 0.1, group = gAdvanced, display = display.none, tooltip = 'Maximum fractional expansion of ATR width at full trend efficiency.')
requireSlope = input.bool(true, 'Require smoother slope confirmation', group = gAdvanced, display = display.none, tooltip = 'Requires the smoother to slope toward the new direction unless strong-reversal bypass applies.')
fastBreak = input.bool(true, 'Strong reversal bypasses slope filter', group = gAdvanced, display = display.none, tooltip = 'Allows a sufficiently large close beyond the threshold to bypass slope confirmation; still waits for candle close.')
breakExtra = input.float(0.5, 'Strong reversal: extra ATR', minval = 0.1, step = 0.1, group = gAdvanced, display = display.none, tooltip = 'Extra ATR distance beyond the effective threshold needed for the slope bypass.')
profileCapacity = input.int(6000, 'Maximum stored volume samples', minval = 1000, maxval = 20000, step = 1000, group = gAdvanced, display = display.none, tooltip = 'Circular buffer. If a required boundary sample was overwritten, the whole profile is skipped. Bigger buffers use more memory/runtime.')
calCapacity = input.int(4000, 'Samples per calendar period', minval = 1000, maxval = 8000, step = 1000, group = gAdvanced, display = display.none, tooltip = 'Default supports a complete 31-day month of 15m candles. Overflow invalidates the entire period. New completed POCs are released after the first source candle of the following period closes, then mapped at the next source open. Trading-session gaps can delay availability further.')

upColor = palette == 'Custom' ? customUp : palette == 'Ice / Coral' ? color.rgb(54, 192, 238) : color.rgb(24, 209, 155)
dnColor = palette == 'Custom' ? customDown : palette == 'Ice / Coral' ? color.rgb(255, 115, 100) : color.rgb(242, 84, 119)
panelBg = uiTheme == 'Dark' ? color.rgb(15, 21, 31) : color.rgb(247, 249, 252)
panelHeader = uiTheme == 'Dark' ? color.rgb(26, 35, 49) : color.rgb(228, 234, 242)
textMain = uiTheme == 'Dark' ? color.rgb(231, 238, 247) : color.rgb(32, 44, 61)
textMuted = uiTheme == 'Dark' ? color.rgb(139, 154, 175) : color.rgb(98, 113, 132)
profileVisible = showProfile and viewPreset != 'Minimal'
mapLabelsVisible = showDailyLabels and viewPreset != 'Minimal'
fullPanel = showPanel and viewPreset == 'Analysis'
metricsVisible = showStats and viewPreset == 'Analysis'

// Constant cost approximation: fees/slippage per side + one full spread.
// No leverage, compounding, funding, financing, actual fills or liquidation model.
roundTripCost = feePct * 2.0 + spreadPct + slipPct * 2.0
costsSet = roundTripCost > 0
amber = color.rgb(240, 184, 76)

f_threshold(_side, _trail, _extreme) =>
    costDistance = math.abs(_extreme) * roundTripCost * costBuffer / 100.0
    costLevel = _extreme - _side * costDistance
    costFilter ? _side == 1 ? math.min(_trail, costLevel) : math.max(_trail, costLevel) : _trail

// Source-seeded initialization avoids the original zero-seed startup transient.
f_jma(_src, _len, _phase, _power) =>
    pr = _phase / 100.0 + 1.5
    beta = 0.45 * (_len - 1) / (0.45 * (_len - 1) + 2.0)
    alpha = math.pow(beta, _power)
    float e0 = na
    float e1 = na
    float e2 = na
    float jm = na
    e0 := (1.0 - alpha) * _src + alpha * nz(e0[1], _src)
    e1 := (_src - e0) * (1.0 - beta) + beta * nz(e1[1], 0.0)
    e2 := (e0 + pr * e1 - nz(jm[1], _src)) * math.pow(1.0 - alpha, 2) + math.pow(alpha, 2) * nz(e2[1], 0.0)
    jm := e2 + nz(jm[1], _src)
    jm

jm = f_jma(src, length, phase, power)
atr = ta.atr(atrLen)
path = ta.sma(math.abs(ta.change(close)), erLen) * erLen
efficiency = not na(path) and path > 0 ? math.min(1.0, math.abs(close - close[erLen]) / path) : 0.0
width = math.max(syminfo.mintick, atr * distance * (adaptive ? 1.0 + expansion * efficiency : 1.0))
isChop = efficiency < chopThreshold
ready = close > 0 and bar_index >= math.max(length * 3, math.max(atrLen, erLen)) and not na(width) and not na(jm[1])

// Profile data: one record per source candle, keyed to its parent chart candle.
type VolumeSample
	int parent
	float hi
	float lo
	float vol
	bool fallback
var array<VolumeSample> volumeSamples = array.new<VolumeSample>()
var int volumeHead = 0
var int droppedParent = na
var array<box> profileBoxes = array.new_box()
var array<int> profileBoxCounts = array.new_int()
var array<line> profileLines = array.new_line()
var array<label> profileTags = array.new_label()
var float latestPOC = na
var int latestPOCConfirmed = na
var string profileStatus = 'Waiting for two endpoints'
var line extendedPOC = na
var label extendedPOCTag = na

f_store_sample(int _head, VolumeSample _sample) =>
    nextHead = _head
    int removedParent = na
    if array.size(volumeSamples) < profileCapacity
        array.push(volumeSamples, _sample)
    else
        removedSample = array.get(volumeSamples, _head)
        removedParent := removedSample.parent
        array.set(volumeSamples, _head, _sample)
        nextHead := (_head + 1) % profileCapacity
        nextHead
    [nextHead, removedParent]

var int diagBars = 0
var int diagFallback = 0
var int diagMissing = 0
var string diagSource = 'Waiting for closed candle'
[vpHigh, vpLow, vpVolume] = request.security_lower_tf(syminfo.tickerid, profileTF, [high, low, volume], ignore_invalid_timeframe = true)
if showPOC and barstate.isconfirmed
    diagBars := diagBars + 1
    diagMissing := diagMissing + (na(volume) or volume < 0 ? 1 : 0)
    intrabars = array.size(vpHigh)
    lowerOK = timeframe.in_seconds(profileTF) < timeframe.in_seconds() and intrabars > 0
    float lowerVolume = 0.0
    if lowerOK
        for i = 0 to intrabars - 1 by 1
            v = array.get(vpVolume, i)
            if na(v) or v < 0 or na(array.get(vpHigh, i)) or na(array.get(vpLow, i)) or array.get(vpHigh, i) < array.get(vpLow, i)
                lowerOK := false
                lowerOK
            lowerVolume := lowerVolume + nz(v)
            lowerVolume
        // Reject materially incomplete intrabar volume rather than undercounting.
        lowerOK := lowerOK and not na(volume) and math.abs(lowerVolume - volume) <= math.max(math.abs(volume) * 0.02, 0.00000001)
        lowerOK
    diagSource := lowerOK ? profileTF + ' TF intrabars' : 'Chart fallback'
    diagFallback := diagFallback + (lowerOK ? 0 : 1)
    if lowerOK
        for i = 0 to intrabars - 1 by 1
            [nextHead, removed] = f_store_sample(volumeHead, VolumeSample.new(time, array.get(vpHigh, i), array.get(vpLow, i), array.get(vpVolume, i), false))
            volumeHead := nextHead
            if not na(removed)
                droppedParent := removed
                droppedParent
    else
        [nextHead, removed] = f_store_sample(volumeHead, VolumeSample.new(time, high, low, volume, true))
        volumeHead := nextHead
        if not na(removed)
            droppedParent := removed
            droppedParent

// Signal-trade interval = entry close to the next reversal close.
// Exact interval extremes are not claims of executable prices or global pivots.
var int side = 0
var float extreme = na
var int extremeTime = na
var int extremeIndex = na
var float trail = na
var float anchorPrice = na
var int anchorTime = na
var bool anchorConfirmed = false
var float entryPrice = na
var int entryTime = na
var int entryTimeClose = na
var int entryIndex = na
var float tradeHigh = na
var float tradeLow = na
var int highTime = na
var int lowTime = na
var int highIndex = na
var int lowIndex = na
var int lastDelay = na
var int tradeNumber = 0
var array<float> netResults = array.new_float()
var array<float> holdBars = array.new_float()
var array<float> holdMinutes = array.new_float()
var array<float> favorable = array.new_float()
var array<float> adverse = array.new_float()
var int wins = 0
var float winningSum = 0.0
var float losingSum = 0.0
var float lastNet = na
var float lastMFE = na
var float lastMAE = na
var int totalCompleted = 0
var array<line> drawings = array.new_line()
var array<color> drawingColors = array.new_color()
var array<int> drawingTrades = array.new_int()
var array<label> markers = array.new_label()
var array<color> markerColors = array.new_color()
var array<color> markerTextColors = array.new_color()
var array<int> markerTrades = array.new_int()
var array<bool> markerSignals = array.new_bool()
var array<label> extremeMarkers = array.new_label()

f_line(_x1, _y1, _x2, _y2, _color, _style, _width) =>
    obj = line.new(_x1, _y1, _x2, _y2, xloc = xloc.bar_time, color = _color, style = _style, width = _width)
    array.push(drawings, obj)
    array.push(drawingColors, _color)
    array.push(drawingTrades, tradeNumber)
    obj

f_label(_x, _y, _text, _style, _color, _textColor, _size, _tooltip) =>
    obj = label.new(_x, _y, _text, xloc = xloc.bar_time, yloc = yloc.price, style = _style, color = _color, textcolor = _textColor, size = _size, tooltip = _tooltip)
    array.push(markers, obj)
    array.push(markerColors, _color)
    array.push(markerTextColors, _textColor)
    array.push(markerTrades, tradeNumber)
    array.push(markerSignals, _style == label.style_label_up or _style == label.style_label_down)
    obj

f_extreme(_x, _y, _text, _style, _color, _tooltip) =>
    obj = label.new(_x, _y, _text, xloc = xloc.bar_time, yloc = yloc.price, style = _style, color = color.new(_color, 100), textcolor = _color, size = size.small, tooltip = _tooltip)
    array.push(extremeMarkers, obj)
    while array.size(extremeMarkers) > extremeHistory * 2
        label.delete(array.shift(extremeMarkers))
    obj

f_price(_value) =>
    na(_value) ? '--' : str.tostring(_value, format.mintick)

// Uniform overlap allocation, O(samples + rows): interior full-row amounts
// accumulate in a difference array instead of a samples-by-rows nested loop.
f_profile(int _start, int _end, int _confirmed) =>
    float result = na
    string status = 'Unavailable'
    int stored = array.size(volumeSamples)
    bool complete = stored > 0 and (na(droppedParent) or droppedParent < _start)
    array<VolumeSample> selected = array.new<VolumeSample>()
    float rangeLow = na
    float rangeHigh = na
    bool hasMissing = false
    bool hasFallback = false
    if complete
        for i = 0 to stored - 1 by 1
            idx = (volumeHead - 1 - i + stored * 2) % stored
            // Before the ring is full, the latest sample is at size - 1.
            idx := stored < profileCapacity ? stored - 1 - i : idx
            rec = array.get(volumeSamples, idx)
            if rec.parent < _start
                break
            if rec.parent <= _end
                array.push(selected, rec)
                rangeLow := na(rangeLow) ? rec.lo : math.min(rangeLow, rec.lo)
                rangeHigh := na(rangeHigh) ? rec.hi : math.max(rangeHigh, rec.hi)
                hasMissing := hasMissing or na(rec.vol) or rec.vol < 0 or na(rec.hi) or na(rec.lo)
                hasFallback := hasFallback or rec.fallback
                hasFallback
    if not complete
        status := 'Skipped: buffer too short'
        status
    else if array.size(selected) == 0 or hasMissing
        status := 'Skipped: missing price/volume'
        status
    else
        stepSize = math.max((rangeHigh - rangeLow) / profileRows, syminfo.mintick)
        // Center a flat-price range in the first row.
        bottom = rangeHigh == rangeLow ? rangeLow - stepSize * 0.5 : rangeLow
        bins = array.new_float(profileRows, 0.0)
        diff = array.new_float(profileRows + 1, 0.0)
        for rec in selected
            first = math.max(0, math.min(profileRows - 1, int(math.floor((rec.lo - bottom) / stepSize))))
            last = math.max(0, math.min(profileRows - 1, int(math.floor((rec.hi - bottom) / stepSize))))
            if rec.hi <= rec.lo or first == last
                array.set(bins, first, array.get(bins, first) + rec.vol)
            else
                density = rec.vol / (rec.hi - rec.lo)
                leftVolume = math.max(0.0, bottom + (first + 1) * stepSize - rec.lo) * density
                rightVolume = math.max(0.0, rec.hi - (bottom + last * stepSize)) * density
                array.set(bins, first, array.get(bins, first) + leftVolume)
                array.set(bins, last, array.get(bins, last) + rightVolume)
                if last - first > 1
                    fullRow = density * stepSize
                    array.set(diff, first + 1, array.get(diff, first + 1) + fullRow)
                    array.set(diff, last, array.get(diff, last) - fullRow)
        float running = 0.0
        float largest = 0.0
        int peak = 0
        for r = 0 to profileRows - 1 by 1
            running := running + array.get(diff, r)
            value = math.max(0.0, array.get(bins, r) + running)
            array.set(bins, r, value)
            if value > largest
                largest := value
                peak := r
                peak
        if largest <= 0
            status := 'Skipped: no positive volume'
            status
        else
            result := bottom + (peak + 0.5) * stepSize
            status := hasFallback ? 'Chart/mixed approximation' : 'LTF approximation'
            while array.size(profileLines) >= profileKeep
                line.delete(array.shift(profileLines))
                label.delete(array.shift(profileTags))
                oldBoxes = array.shift(profileBoxCounts)
                if oldBoxes > 0
                    for j = 0 to oldBoxes - 1 by 1
                        box.delete(array.shift(profileBoxes))
            int made = 0
            if profileVisible
                for r = 0 to profileRows - 1 by 1
                    weight = array.get(bins, r) / largest
                    rightTime = _start + int((_end - _start) * profileWidth * weight)
                    rowBottom = bottom + r * stepSize
                    shade = r == peak ? color.new(amber, 55) : color.new(textMuted, profileOpacity)
                    rowBox = box.new(_start, rowBottom + stepSize, rightTime, rowBottom, xloc = xloc.bar_time, border_color = color.new(amber, 100), bgcolor = shade)
                    array.push(profileBoxes, rowBox)
                    made := made + 1
                    made
            array.push(profileBoxCounts, made)
            array.push(profileLines, line.new(_start, result, _end, result, xloc = xloc.bar_time, color = amber, width = 2))
            tip = 'CONFIRMED SWING POC (estimate)\n' + status + '\nConfirmation close (feed timezone): ' + str.format_time(_confirmed, 'yyyy-MM-dd HH:mm', syminfo.timezone) + '\n' + str.tostring(array.size(selected)) + ' source candles; ' + str.tostring(profileRows) + ' rows.\nFull endpoint candles included. Highest-volume row midpoint; uniform OHLC allocation.\nKnown only on reversal confirmation, not at the historical endpoint.'
            array.push(profileTags, label.new(_end, result, 'POC  ' + f_price(result), xloc = xloc.bar_time, style = label.style_label_left, color = color.new(amber, 100), textcolor = amber, size = size.tiny, tooltip = tip))
    [result, status]

bool buy = false
bool sell = false
bool reversed = false

if ready and barstate.isconfirmed
    if side == 0
        if jm != jm[1]
            side := jm > jm[1] ? 1 : -1
            buy := side == 1
            sell := side == -1
            anchorPrice := close
            anchorTime := time
            anchorTime
    // Include all candles AFTER the entry close, including the exit candle.
    else // Equal highs/lows retain their first occurrence.
        if high > tradeHigh
            tradeHigh := high
            highTime := time
            highIndex := bar_index
            highIndex
        if low < tradeLow
            tradeLow := low
            lowTime := time
            lowIndex := bar_index
            lowIndex
        extreme := side == 1 ? tradeHigh : tradeLow
        extremeTime := side == 1 ? highTime : lowTime
        extremeIndex := side == 1 ? highIndex : lowIndex
        trail := side == 1 ? math.max(trail, extreme - width) : math.min(trail, extreme + width)
        slopeOK = not requireSlope or (side == 1 ? jm < jm[1] : jm > jm[1])
        trigger = f_threshold(side, trail, extreme)
        hardBreak = fastBreak and (side == 1 ? close < trigger - atr * breakExtra : close > trigger + atr * breakExtra)
        crossed = side == 1 ? close < trigger : close > trigger
        if crossed and (slopeOK or hardBreak)
            reversed := true
            // Use confirmation CLOSE prices, never ZigZag extremes, for returns.
            grossReturn = side * (close - entryPrice) / entryPrice * 100.0
            lastNet := grossReturn - roundTripCost
            lastMFE := math.max(0.0, (side == 1 ? tradeHigh - entryPrice : entryPrice - tradeLow) / entryPrice * 100.0)
            lastMAE := math.max(0.0, (side == 1 ? entryPrice - tradeLow : tradeHigh - entryPrice) / entryPrice * 100.0)
            array.push(netResults, lastNet)
            array.push(holdBars, float(bar_index - entryIndex))
            array.push(holdMinutes, (time_close - entryTimeClose) / 60000.0)
            array.push(favorable, lastMFE)
            array.push(adverse, lastMAE)
            totalCompleted := totalCompleted + 1
            wins := wins + (lastNet > 0 ? 1 : 0)
            winningSum := winningSum + math.max(lastNet, 0.0)
            losingSum := losingSum + math.max(-lastNet, 0.0)
            if array.size(netResults) > statsWindow
                removed = array.shift(netResults)
                wins := wins - (removed > 0 ? 1 : 0)
                winningSum := math.max(0.0, winningSum - math.max(removed, 0.0))
                losingSum := math.max(0.0, losingSum - math.max(-removed, 0.0))
                array.shift(holdBars)
                array.shift(holdMinutes)
                array.shift(favorable)
                array.shift(adverse)
            legColor = side == 1 ? upColor : dnColor
            lastDelay := bar_index - extremeIndex
            if showZZ and extremeTime >= anchorTime
                // The first seed-to-extreme segment is dashed, not a confirmed pivot pair.
                legStyle = anchorConfirmed ? line.style_solid : line.style_dashed
                if showGlow
                    f_line(anchorTime, anchorPrice, extremeTime, extreme, color.new(legColor, 88), legStyle, zigWidth + 6)
                f_line(anchorTime, anchorPrice, extremeTime, extreme, legColor, legStyle, zigWidth)
                f_label(extremeTime, extreme, '•', label.style_none, color.new(legColor, 100), legColor, size.small, 'Swing endpoint confirmed ' + str.tostring(lastDelay) + ' bar(s) later. Signal is on the confirmation candle.')
            if showExtremes
                intervalTip = 'Signal trade #' + str.tostring(tradeNumber) + ' | ' + (side == 1 ? 'LONG' : 'SHORT') + '\nEntry close: ' + f_price(entryPrice) + '\nExit close: ' + f_price(close) + '\nDuration: ' + str.tostring(bar_index - entryIndex) + ' bars\nModeled net: ' + str.tostring(lastNet, '#.###') + '%\nMFE / MAE: ' + str.tostring(lastMFE, '#.###') + '% / ' + str.tostring(lastMAE, '#.###') + '%\nConfirmed on this trade\'s exit. Historical extreme, not an entry/exit fill. Entry candle pre-entry wicks excluded.'
                f_extreme(highTime, tradeHigh, 'H  ' + f_price(tradeHigh), label.style_label_down, dnColor, intervalTip + '\nHIGH occurred ' + str.tostring(bar_index - highIndex) + ' bar(s) before confirmation.')
                f_extreme(lowTime, tradeLow, 'L  ' + f_price(tradeLow), label.style_label_up, upColor, intervalTip + '\nLOW occurred ' + str.tostring(bar_index - lowIndex) + ' bar(s) before confirmation.')
            if showTradePath
                f_line(entryTime, entryPrice, time, close, color.new(color.silver, 45), line.style_dotted, 1)
            if showPOC and anchorConfirmed and extremeTime > anchorTime
                [newPOC, newStatus] = f_profile(anchorTime, extremeTime, time_close)
                profileStatus := newStatus + (na(newPOC) and not na(latestPOC) ? ' | Prior valid POC retained' : '')
                if not na(newPOC)
                    latestPOC := newPOC
                    latestPOCConfirmed := time_close
                    line.delete(extendedPOC)
                    extendedPOC := line.new(time_close, newPOC, time_close + 1, newPOC, xloc = xloc.bar_time, extend = extend.right, style = line.style_dotted, color = color.new(amber, 25), width = 1)
                    extendedPOC
            anchorPrice := extreme
            anchorTime := extremeTime
            anchorConfirmed := true
            side := -side
            buy := side == 1
            sell := side == -1
            sell

    if buy or sell
        tradeNumber := tradeNumber + 1
        entryPrice := close
        entryTime := time
        entryTimeClose := time_close
        entryIndex := bar_index
        // A close-time signal cannot own the earlier wicks of this candle.
        tradeHigh := close
        tradeLow := close
        highTime := time
        lowTime := time
        highIndex := bar_index
        lowIndex := bar_index
        extreme := close
        extremeTime := time
        extremeIndex := bar_index
        trail := side == 1 ? close - width : close + width
        if showSignals
            signalText = (buy ? 'BUY' : 'SELL') + (showSignalPrice ? '\n' + f_price(close) : '')
            signalTip = (reversed ? 'Close opposite position, then request ' : 'Initial signal: request ') + (buy ? 'LONG' : 'SHORT') + '\nSignal #' + str.tostring(tradeNumber) + ' | Close: ' + f_price(close) + '\nCandle-close signal. This price is a reference, not a broker fill.'
            signalY = buy ? low - atr * markerGap : high + atr * markerGap
            f_line(time, close, time, signalY, color.new(buy ? upColor : dnColor, 70), line.style_dotted, 1)
            f_label(time, signalY, signalText, buy ? label.style_label_up : label.style_label_down, buy ? upColor : dnColor, buy ? color.rgb(9, 24, 31) : color.white, size.small, signalTip)
        // Explicit object limits keep history bounded and latest levels intact.
        while array.size(drawings) > history * ((showZZ ? showGlow ? 2 : 1 : 0) + (showTradePath ? 1 : 0) + (showSignals ? 1 : 0))
            line.delete(array.shift(drawings))
            array.shift(drawingColors)
            array.shift(drawingTrades)
        while array.size(markers) > history * ((showZZ ? 1 : 0) + (showSignals ? 1 : 0)) + (showSignals ? 1 : 0)
            label.delete(array.shift(markers))
            array.shift(markerColors)
            array.shift(markerTextColors)
            array.shift(markerTrades)
            array.shift(markerSignals)
        // Restore original colors before applying age fade. Recent objects stay crisp.
        if array.size(drawings) > 0
            for i = 0 to array.size(drawings) - 1 by 1
                baseColor = array.get(drawingColors, i)
                faded = array.get(drawingTrades, i) < tradeNumber - focusTrades
                line.set_color(array.get(drawings, i), color.new(baseColor, faded ? math.max(color.t(baseColor), historyFade) : color.t(baseColor)))
        if array.size(markers) > 0
            for i = 0 to array.size(markers) - 1 by 1
                baseColor = array.get(markerColors, i)
                baseText = array.get(markerTextColors, i)
                faded = array.get(markerTrades, i) < tradeNumber - focusTrades
                hidden = array.get(markerSignals, i) and array.get(markerTrades, i) <= tradeNumber - signalHistory
                label.set_color(array.get(markers, i), color.new(baseColor, hidden ? 100 : faded ? math.max(color.t(baseColor), historyFade) : color.t(baseColor)))
                label.set_textcolor(array.get(markers, i), color.new(baseText, hidden ? 100 : faded ? historyFade : color.t(baseText)))

activeThreshold = side != 0 ? f_threshold(side, trail, extreme) : na
// Recompute a preview from uncommitted OHLC, with no mutation of trade state.
previewExtreme = side == 1 ? math.max(nz(extreme, high), high) : math.min(nz(extreme, low), low)
previewTrail = side == 1 ? math.max(nz(trail, previewExtreme - width), previewExtreme - width) : math.min(nz(trail, previewExtreme + width), previewExtreme + width)
previewThreshold = f_threshold(side, previewTrail, previewExtreme)
previewSlopeOK = not requireSlope or (side == 1 ? jm < jm[1] : jm > jm[1])
previewHardBreak = fastBreak and (side == 1 ? close < previewThreshold - atr * breakExtra : close > previewThreshold + atr * breakExtra)
previewNear = side == 1 ? close <= previewThreshold + atr * warningATR : close >= previewThreshold - atr * warningATR
warning = earlyWarning and ready and side != 0 and barstate.isrealtime and not barstate.isconfirmed and previewNear and (previewSlopeOK or previewHardBreak)
// Sideways classification is informational: it never creates a flat state.
bgcolor(shadeChop and ready and isChop ? color.new(amber, 97) : na, title = 'Sideways caution')
trendColor = side == 1 ? upColor : side == -1 ? dnColor : color.gray
pJ = plot(showJurik ? jm : na, 'Jurik', color = color.new(trendColor, 10), linewidth = 2)
pC = plot(close, 'Fill reference', display = display.none)
fill(pJ, pC, color = showZone ? color.new(trendColor, 96) : na, title = 'Jurik ribbon')
pLong = plot(showTrail and side == 1 and not buy ? activeThreshold : na, 'Long confirmation threshold', color = color.new(upColor, 20), linewidth = 1, style = plot.style_linebr)
pShort = plot(showTrail and side == -1 and not sell ? activeThreshold : na, 'Short confirmation threshold', color = color.new(dnColor, 20), linewidth = 1, style = plot.style_linebr)
fill(pLong, pC, color = showZone ? color.new(upColor, 97) : na, title = 'Long zone')
fill(pShort, pC, color = showZone ? color.new(dnColor, 97) : na, title = 'Short zone')
barcolor(paintBars and side != 0 ? color.new(trendColor, 12) : na)
plot(buy or sell ? close : na, 'Confirmed signal price', color = trendColor, linewidth = 2, style = plot.style_circles)

var label warningTag = na
var line developing = na
var label liveTag = na
var line entryLevel = na
var label entryTag = na
var line confirmLevel = na
var label confirmTag = na
if barstate.islast
    label.delete(warningTag)
    if warning and showWarningLabel
        warningTag := label.new(bar_index, side == 1 ? high + atr * 0.6 : low - atr * 0.6, side == 1 ? 'SHORT WATCH\nUNCONFIRMED' : 'LONG WATCH\nUNCONFIRMED', style = side == 1 ? label.style_label_down : label.style_label_up, color = amber, textcolor = color.black, size = size.small, tooltip = 'Provisional intrabar warning only. Can vanish before candle close; never an execution alert.')
        warningTag
    line.delete(developing)
    label.delete(liveTag)
    line.delete(entryLevel)
    label.delete(entryTag)
    line.delete(confirmLevel)
    label.delete(confirmTag)
    if showLive and side != 0 and not na(anchorTime)
        // Do not attach the entry candle's pre-entry wick to the new trade.
        liveExtreme = bar_index > entryIndex ? side == 1 ? math.max(extreme, high) : math.min(extreme, low) : extreme
        liveTime = liveExtreme != extreme ? time : extremeTime
        developing := line.new(anchorTime, anchorPrice, liveTime, liveExtreme, xloc = xloc.bar_time, color = color.new(trendColor, 5), style = line.style_dotted, width = 2)
        if showLiveText
            liveTag := label.new(liveTime, liveExtreme, 'LIVE SWING  ' + f_price(liveExtreme), xloc = xloc.bar_time, style = side == 1 ? label.style_label_down : label.style_label_up, color = color.new(trendColor, 100), textcolor = trendColor, size = size.tiny, tooltip = 'Unconfirmed swing. This endpoint can move. It does not trigger an order.')
            liveTag
    if showLevels and side != 0
        entryLevel := line.new(entryTime, entryPrice, time + 1, entryPrice, xloc = xloc.bar_time, extend = extend.right, color = color.new(color.silver, 30), style = line.style_dashed)
        levelsClose = math.abs(entryPrice - activeThreshold) < atr * 0.65
        entryText = 'ENTRY  ' + f_price(entryPrice) + (levelsClose ? '\nREVERSE  ' + f_price(activeThreshold) : '')
        entryTag := label.new(bar_index + labelOffset, entryPrice, entryText, style = label.style_label_left, color = panelHeader, textcolor = textMain, size = size.small)
        confirmLevel := line.new(time, activeThreshold, time + 1, activeThreshold, xloc = xloc.bar_time, extend = extend.right, color = color.new(trendColor, 35), style = line.style_dotted)
        if not levelsClose
            confirmTag := label.new(bar_index + labelOffset, activeThreshold, 'REVERSE  ' + f_price(activeThreshold), style = label.style_label_left, color = panelHeader, textcolor = trendColor, size = size.small, tooltip = 'Close must cross this level and pass the slope filter, or the strong-reversal bypass. Includes the cost floor when enabled. This is not a broker stop order.')
            confirmTag

plot(buy or sell ? time_close : na, 'Audit / confirmed event close timestamp (ms)', display = display.data_window)
plot(buy ? 1 : sell ? -1 : na, 'Audit / confirmed event direction', display = display.data_window)
plot(side, 'Signal direction: 1 long / -1 short', display = display.data_window)
plot(entryPrice, 'Latest entry close', display = display.data_window)
plot(tradeHigh, 'Active signal-trade HIGH (closed candles)', display = display.data_window)
plot(tradeLow, 'Active signal-trade LOW (closed candles)', display = display.data_window)
type AtlasCalendarProfile
	array<float> highs
	array<float> lows
	array<float> volumes
	int start = na
	bool boundaryObserved = false
	bool invalid = false
	string reason = 'Waiting for full period'
	float lo = na
	float hi = na
	float total = 0.0
	float poc = na
	int available = na
	string status = 'Waiting for full period'

f_cal_new() =>
    AtlasCalendarProfile.new(array.new_float(), array.new_float(), array.new_float())

// Uniform candle-volume overlap in O(samples + rows), not samples * rows.
// Edge bins receive exact partial overlap; complete interior bins receive a
// constant density increment through a difference array / prefix sum.
f_cal_poc(AtlasCalendarProfile p, int rows) =>
    float result = na
    int count = array.size(p.highs)
    if count > 0 and p.total > 0 and not na(p.lo) and not na(p.hi)
        if p.hi <= p.lo
            result := p.lo
            result
        else
            float stepSize = (p.hi - p.lo) / rows
            array<float> edges = array.new_float(rows, 0.0)
            array<float> delta = array.new_float(rows + 1, 0.0)
            for i = 0 to count - 1 by 1
                float h = array.get(p.highs, i)
                float l = array.get(p.lows, i)
                float v = array.get(p.volumes, i)
                int a = math.max(0, math.min(rows - 1, int(math.floor((l - p.lo) / stepSize))))
                int b = math.max(0, math.min(rows - 1, int(math.floor((h - p.lo) / stepSize))))
                if a == b or h <= l
                    array.set(edges, a, array.get(edges, a) + v)
                else
                    float density = v / (h - l)
                    float firstAmount = density * math.max(0.0, p.lo + (a + 1) * stepSize - l)
                    float lastAmount = density * math.max(0.0, h - (p.lo + b * stepSize))
                    array.set(edges, a, array.get(edges, a) + firstAmount)
                    array.set(edges, b, array.get(edges, b) + lastAmount)
                    if b > a + 1
                        float fullAmount = density * stepSize
                        array.set(delta, a + 1, array.get(delta, a + 1) + fullAmount)
                        array.set(delta, b, array.get(delta, b) - fullAmount)
            float running = 0.0
            float largest = -1.0
            int winning = 0
            for row = 0 to rows - 1 by 1
                running := running + array.get(delta, row)
                float amount = math.max(0.0, array.get(edges, row) + running)
                if amount > largest
                    largest := amount
                    winning := row
                    winning
            result := p.lo + (winning + 0.5) * stepSize
            result
    result

method calUpdate(AtlasCalendarProfile p, bool enabled, bool supported, int periodStart, int periodEnd, int capacity, int rows) =>
    if not enabled
        p.status := 'OFF'
        p.status
    else if not supported
        p.status := 'Unsupported chart / timeframe'
        p.status
    else
        if na(p.start)
            p.start := periodStart
            p.boundaryObserved := false
            p.boundaryObserved
        else if periodStart != p.start
            // Clear stale POC if the most recent completed period is invalid.
            p.poc := na
            p.available := na
            if not p.boundaryObserved
                p.status := 'First partial period skipped'
                p.status
            else if p.invalid
                p.status := p.reason
                p.status
            else if p.total <= 0 or array.size(p.highs) == 0
                p.status := 'No positive volume; period skipped'
                p.status
            else
                p.poc := f_cal_poc(p, rows)
                p.available := not na(p.poc) ? time_close : na
                p.status := 'Completed ' + str.format_time(p.start, 'yyyy-MM-dd', syminfo.timezone) + ' / source-volume estimate'
                p.status
            array.clear(p.highs)
            array.clear(p.lows)
            array.clear(p.volumes)
            p.start := periodStart
            p.boundaryObserved := true
            p.invalid := false
            p.reason := ''
            p.lo := na
            p.hi := na
            p.total := 0.0
            p.total
        // Never assign a chart candle spanning a calendar boundary to one side.
        // Reject its period and reject the next one if it opened after boundary
        // due to a previous spanning candle. That next period lacks its opening.
        bool previousSpanned = not na(time_close[1]) and time_close[1] > periodStart and time[1] < periodStart
        if na(periodStart) or na(periodEnd) or na(time_close) or time < periodStart or time_close > periodEnd or previousSpanned
            p.invalid := true
            p.reason := 'Boundary-spanning candle; period skipped'
            p.reason
        else if na(volume) or volume < 0 or na(high) or na(low) or high < low
            p.invalid := true
            p.reason := 'Missing / invalid volume or OHLC; period skipped'
            p.reason
        else if array.size(p.highs) >= capacity
            p.invalid := true
            p.reason := 'Sample capacity exceeded; period skipped'
            p.reason
        if not p.invalid and p.boundaryObserved
            array.push(p.highs, high)
            array.push(p.lows, low)
            array.push(p.volumes, volume)
            p.lo := na(p.lo) ? low : math.min(p.lo, low)
            p.hi := na(p.hi) ? high : math.max(p.hi, high)
            p.total := p.total + volume
            p.total
        if p.invalid
            array.clear(p.highs)
            array.clear(p.lows)
            array.clear(p.volumes)
    0

// Execute accumulation INSIDE each requested source context. Return scalars only.
// The offset exposes committed source state, not the unfinished source candle.
f_calendar(string period, bool enabled) =>
    var AtlasCalendarProfile p = f_cal_new()
    p.calUpdate(enabled, true, time(period), time_close(period), calCapacity, calRows)
    float value = p.poc
    int available = p.available
    string status = p.status
    // Developing values are only consumed by the latest chart map. Restrict the
    // expensive read-only profile calculation to the last three SOURCE bars;
    // retain a prior-bar result for [1]. This gate never touches accumulation,
    // completed POCs, historical signals or alerts.
    float developing = enabled and showDevelopingPOC and bar_index >= last_bar_index - 2 and p.boundaryObserved and not p.invalid ? f_cal_poc(p, calRows) : na
    int developingPeriod = p.start
    [value[1], available[1], status[1], developing[1], developingPeriod[1]]

[calDRaw, calDFrom, calDRawStatus, calDRawDev, calDDevPeriod] = request.security(syminfo.tickerid, calSource, f_calendar('D', showCalendarPOC and calDEnabled), lookahead = barmerge.lookahead_on, calc_bars_count = 6000)
[calWRaw, calWFrom, calWRawStatus, calWRawDev, calWDevPeriod] = request.security(syminfo.tickerid, calSource, f_calendar('W', showCalendarPOC and calWEnabled), lookahead = barmerge.lookahead_on, calc_bars_count = 6000)
[calMRaw, calMFrom, calMRawStatus, calMRawDev, calMDevPeriod] = request.security(syminfo.tickerid, calSource, f_calendar('M', showCalendarPOC and calMEnabled), lookahead = barmerge.lookahead_on, calc_bars_count = 6000)
calSupported = chart.is_standard and timeframe.in_seconds() <= timeframe.in_seconds(calSource)
calDPOC = calSupported and showCalendarPOC and calDEnabled ? calDRaw : na
calWPOC = calSupported and showCalendarPOC and calWEnabled ? calWRaw : na
calMPOC = calSupported and showCalendarPOC and calMEnabled ? calMRaw : na
calDDev = calSupported and calDDevPeriod == time('D') ? calDRawDev : na
calWDev = calSupported and calWDevPeriod == time('W') ? calWRawDev : na
calMDev = calSupported and calMDevPeriod == time('M') ? calMRawDev : na
calDStatus = not calSupported ? 'Use standard chart <= calendar source' : na(calDRawStatus) ? 'Waiting for source history' : calDRawStatus
calWStatus = not calSupported ? 'Use standard chart <= calendar source' : na(calWRawStatus) ? 'Waiting for source history' : calWRawStatus
calMStatus = not calSupported ? 'Use standard chart <= calendar source' : na(calMRawStatus) ? 'Waiting for source history' : calMRawStatus

// Developing values from the prior calendar period are hidden until the first
// closed source sample of the new period is available. Never relabel old data.

// Last closed chart-bar Supertrend. Native direction < 0 means bullish.
[stRaw, stDirectionRaw] = ta.supertrend(stFactor, stLength)
var float stClosed = na
var int stDirection = 0
if barstate.isconfirmed
    stClosed := stRaw
    stDirection := na(stRaw) ? 0 : stDirectionRaw < 0 ? 1 : -1
    stDirection
plot(showST and stDirection == 1 ? stClosed : na, 'Supertrend bullish / closed', color = color.new(upColor, 40), linewidth = 1, style = plot.style_linebr)
plot(showST and stDirection == -1 ? stClosed : na, 'Supertrend bearish / closed', color = color.new(dnColor, 40), linewidth = 1, style = plot.style_linebr)

// Session identity is a local calendar start timestamp, not a false->true edge.
// This catches 24h resets and keeps overnight sessions intact across midnight.
sessTZ = sessionZone == 'Exchange' ? syminfo.timezone : sessionZone
sessStartH = int(str.tonumber(str.substring(sessionHours, 0, 2)))
sessStartM = int(str.tonumber(str.substring(sessionHours, 2, 4)))
sessEndH = int(str.tonumber(str.substring(sessionHours, 5, 7)))
sessEndM = int(str.tonumber(str.substring(sessionHours, 7, 9)))
sessStartMinute = sessStartH * 60 + sessStartM
sessEndMinute = sessEndH * 60 + sessEndM
sessLocalMinute = hour(time, sessTZ) * 60 + minute(time, sessTZ)
sessDay = dayofmonth(time, sessTZ) - (sessLocalMinute < sessStartMinute ? 1 : 0)
sessKeyNow = timestamp(sessTZ, year(time, sessTZ), month(time, sessTZ), sessDay, sessStartH, sessStartM)
sessEndNow = timestamp(sessTZ, year(time, sessTZ), month(time, sessTZ), sessDay + (sessEndMinute <= sessStartMinute ? 1 : 0), sessEndH, sessEndM)
sessSupported = chart.is_standard and timeframe.isintraday
sessIn = not na(time(timeframe.period, sessionHours + ':' + sessionDays, sessTZ))
var int sessKey = na
var int sessEnd = na
var int sessLastClose = na
var bool sessActive = false
var bool sessCoverage = false
var bool sessVolumeOK = true
var float sessOpen = na
var float sessHigh = na
var float sessLow = na
var float sessPV = 0.0
var float sessVolume = 0.0
var float sessPrevHigh = na
var float sessPrevLow = na
var string sessPriorStatus = 'Waiting for complete session'
if showSession and sessSupported and barstate.isconfirmed
    if sessActive and (not sessIn or sessKeyNow != sessKey)
        completedOK = sessCoverage and sessLastClose == sessEnd
        sessPrevHigh := completedOK ? sessHigh : na
        sessPrevLow := completedOK ? sessLow : na
        sessPriorStatus := completedOK ? 'Completed ' + str.format_time(sessKey, 'yyyy-MM-dd HH:mm', sessTZ) : 'Previous session incomplete / skipped'
        sessActive := false
        sessActive
    if sessIn
        if not sessActive
            // First loaded session is deliberately excluded, even at its start.
            sessCoverage := not na(sessKey) and time == sessKeyNow
            sessKey := sessKeyNow
            sessEnd := sessEndNow
            sessActive := true
            sessVolumeOK := true
            sessOpen := open
            sessHigh := high
            sessLow := low
            sessPV := 0.0
            sessVolume := 0.0
            sessVolume
        if time < sessKey or na(time_close) or time_close > sessEnd or na(high) or na(low) or high < low
            sessCoverage := false
            sessCoverage
        else
            sessHigh := math.max(sessHigh, high)
            sessLow := math.min(sessLow, low)
            if na(volume) or volume < 0 or na(hlc3)
                sessVolumeOK := false
                sessVolumeOK
            else
                sessPV := sessPV + hlc3 * volume
                sessVolume := sessVolume + volume
                sessVolume
        sessLastClose := time_close
        sessLastClose
sessVWAP = showSession and sessSupported and sessActive and sessCoverage and sessVolumeOK and sessVolume > 0 ? sessPV / sessVolume : na
sessOpenKnown = showSession and sessSupported and sessActive and sessCoverage ? sessOpen : na
sessStatus = not showSession ? 'OFF' : not sessSupported ? 'STANDARD INTRADAY ONLY' : not sessActive ? 'OUTSIDE SESSION' : not sessCoverage ? 'PARTIAL / UNALIGNED' : not sessVolumeOK ? 'VOLUME INVALID' : sessVolume <= 0 ? 'NO POSITIVE VOLUME' : 'CLOSED CANDLES'
sessPlotNow = sessIn and sessKey == sessKeyNow
plot(showSessionVWAP and sessPlotNow ? sessVWAP : na, 'Custom session VWAP / closed candles', color = color.rgb(225, 199, 126), linewidth = 2, style = plot.style_linebr)
plot(showSessionLevels and viewPreset != 'Minimal' and sessPlotNow ? sessOpenKnown : na, 'Custom session open', color = color.new(textMuted, 25), style = plot.style_stepline)
plot(showSession and showSessionLevels and viewPreset != 'Minimal' ? sessPrevHigh : na, 'Previous complete session high', color = color.new(dnColor, 50), style = plot.style_stepline)
plot(showSession and showSessionLevels and viewPreset != 'Minimal' ? sessPrevLow : na, 'Previous complete session low', color = color.new(upColor, 50), style = plot.style_stepline)

// Previous completed candles are offset with lookahead_on. Current OPEN/time
// are already established at period start; unfinished H/L/C are never requested.
[dOpenPrev, dHighPrev, dLowPrev, dClosePrev, dATRPrev] = request.security(syminfo.tickerid, 'D', [open[1], high[1], low[1], close[1], ta.atr(14)[1]], lookahead = barmerge.lookahead_on)
[wOpenPrev, wHighPrev, wLowPrev, wClosePrev] = request.security(syminfo.tickerid, 'W', [open[1], high[1], low[1], close[1]], lookahead = barmerge.lookahead_on)
[mOpenPrev, mHighPrev, mLowPrev, mClosePrev] = request.security(syminfo.tickerid, 'M', [open[1], high[1], low[1], close[1]], lookahead = barmerge.lookahead_on)
[dOpenNow, dStart, dEnd] = request.security(syminfo.tickerid, 'D', [open, time, time_close], lookahead = barmerge.lookahead_on)
[wOpenNow, wStart, wEnd] = request.security(syminfo.tickerid, 'W', [open, time, time_close], lookahead = barmerge.lookahead_on)
[mOpenNow, mStart, mEnd] = request.security(syminfo.tickerid, 'M', [open, time, time_close], lookahead = barmerge.lookahead_on)
dailySupported = timeframe.in_seconds() <= timeframe.in_seconds('D')
weeklySupported = timeframe.in_seconds() <= timeframe.in_seconds('W')
monthlySupported = timeframe.in_seconds() <= timeframe.in_seconds('M')
dBody = dClosePrev - dOpenPrev
dRange = dHighPrev - dLowPrev
bodyValid = not na(dBody) and dRange > 0 and math.abs(dBody) >= syminfo.mintick and math.abs(dBody) >= dRange * minBodyRatio
mapDailyColor = color.rgb(116, 157, 209)
mapWeeklyColor = color.rgb(66, 188, 194)
mapMonthlyColor = color.rgb(174, 145, 225)
// Family keys group correlated ratios. Focus can hide distant pinned references.
type AtlasReference
	float price
	string name
	string family
	color tint
	string pattern
	int weight
	int stamp
	string tip
	bool pinned
	bool fixedReference

var array<AtlasReference> atlasRefs = array.new<AtlasReference>()
var array<line> atlasMapLines = array.new_line()
var array<label> atlasMapTags = array.new_label()
var array<box> atlasMapZones = array.new_box()

// Reuse committed drawing IDs with setters. Reapply setters on every last-bar
// execution because realtime rollback restores committed object properties.
// Grow only when needed; trim unused slots after rendering, including zero refs.
f_map_line(int slot, AtlasReference ref) =>
    if slot >= array.size(atlasMapLines)
        array.push(atlasMapLines, line.new(ref.stamp, ref.price, math.max(time, ref.stamp + 1), ref.price, xloc = xloc.bar_time, extend = extend.right))
    obj = array.get(atlasMapLines, slot)
    line.set_xy1(obj, ref.stamp, ref.price)
    line.set_xy2(obj, math.max(time, ref.stamp + 1), ref.price)
    line.set_color(obj, color.new(ref.tint, 22))
    line.set_style(obj, ref.pattern)
    line.set_width(obj, ref.weight)
    0

f_map_tag(int slot, int x, float y, string caption, color tint, int transparency, string tip) =>
    if slot >= array.size(atlasMapTags)
        array.push(atlasMapTags, label.new(x, y, '', style = label.style_label_left, size = size.tiny))
    obj = array.get(atlasMapTags, slot)
    label.set_xy(obj, x, y)
    label.set_text(obj, caption)
    label.set_color(obj, color.new(panelBg, transparency))
    label.set_textcolor(obj, tint)
    label.set_tooltip(obj, tip)
    0

f_map_zone(int slot, int stamp, float top, float bottom) =>
    if slot >= array.size(atlasMapZones)
        array.push(atlasMapZones, box.new(stamp, top, math.max(time, stamp + 1), bottom, xloc = xloc.bar_time, extend = extend.right, border_color = color.new(amber, 75), bgcolor = color.new(amber, 92)))
    obj = array.get(atlasMapZones, slot)
    box.set_lefttop(obj, stamp, top)
    box.set_rightbottom(obj, math.max(time, stamp + 1), bottom)
    0

mapAnchor = mapSelection == 'Live price' or barstate.isconfirmed ? close : nz(close[1], close)
mapRadius = mapFocus and not mapShowAll and not na(dATRPrev) and dATRPrev > 0 ? dATRPrev * mapFocusATR : na
f_map_in_focus(float price) =>
    na(mapRadius) or math.abs(price - mapAnchor) <= mapRadius

f_map_distance(float price) =>
    na(price) ? '--' : f_price(math.abs(price - mapAnchor)) + (not na(dATRPrev) and dATRPrev > 0 ? ' / ' + str.tostring(math.abs(price - mapAnchor) / dATRPrev, '#.##') + ' daily ATR' : '')

f_map_add(_p, _name, _family, _color, _style, _weight, _stamp, _tip, _pinned, _fixed) =>
    if not na(_p) and not na(_stamp)
        array.push(atlasRefs, AtlasReference.new(_p, _name, _family, _color, _style, _weight, _stamp, _tip, _pinned, _fixed))
    0

f_map_period(_prefix, _enabled, _openOn, _retOn, _extOn, _rangeOn, _o, _h, _l, _c, _now, _stamp, _color, _weight) =>
    if _enabled
        b = _c - _o
        r = _h - _l
        valid = not na(b) and r > 0 and math.abs(b) >= syminfo.mintick and math.abs(b) >= r * minBodyRatio
        if _openOn
            f_map_add(_now, _prefix + ' OPEN', _prefix + ' Open', _color, line.style_dashed, _weight, _stamp, 'Current period open; fixed at the symbol/feed period start.', true, false)
        if valid and _retOn
            for q in array.from(0.382, 0.5, 0.618)
                f_map_add(_c - q * b, _prefix + ' RET ' + str.tostring(q), _prefix + ' Body', _color, line.style_dotted, _weight, _stamp, 'Previous completed candle BODY: C - ratio*(C-O). Signed body; not a forecast.', false, true)
        if valid and _extOn
            for q in array.from(1.272, 1.618, 2.0)
                f_map_add(_o + q * b, _prefix + ' EXT ' + str.tostring(q), _prefix + ' Body', _color, line.style_solid, _weight, _stamp, 'Previous completed candle BODY: O + ratio*(C-O). Not an ABC extension or an order.', false, true)
        if _rangeOn and not na(r) and r > 0
            for q in array.from(0.618, 1.0, 1.618)
                f_map_add(_now + q * r, _prefix + ' RANGE +' + str.tostring(q), _prefix + ' Range', _color, line.style_dotted, _weight, _stamp, 'Current period open + previous completed full high-low range * ratio. Hypothetical upside distance.', false, true)
                f_map_add(_now - q * r, _prefix + ' RANGE -' + str.tostring(q), _prefix + ' Range', _color, line.style_dotted, _weight, _stamp, 'Current period open - previous completed full high-low range * ratio. Hypothetical downside distance.', false, true)
    0

f_map_poc(_prefix, _enabled, _poc, _from, _status, _developing, _start, _color, _weight) =>
    if showCalendarPOC and _enabled
        f_map_add(_poc, _prefix + ' POC', _prefix + ' POC', _color, line.style_solid, _weight, _from, 'Completed calendar-period POC estimate. ' + _status + '. Line begins when the period becomes available, not at its historical peak.', true, true)
        if showDevelopingPOC
            f_map_add(_developing, _prefix + ' POC DEV', _prefix + ' Developing', color.new(_color, 40), line.style_dashed, 1, _start, 'UNCONFIRMED current-period POC estimate. Changes with incoming samples; excluded from confluence and nearest fixed-reference scenarios. ' + _status, true, false)
    0

float nearestAbovePrice = na
string nearestAboveName = '--'
float nearestBelowPrice = na
string nearestBelowName = '--'
string confluenceSummary = 'NO DISPLAYED CLUSTER'

if barstate.islast or showSnapshots and barstate.isconfirmed and (buy or sell)
    array.clear(atlasRefs)
    f_map_period('D', showDaily and dailySupported, showDailyOpen, showBodyRetrace, showBodyExtend, showRangeTargets, dOpenPrev, dHighPrev, dLowPrev, dClosePrev, dOpenNow, dStart, mapDailyColor, 1)
    f_map_period('W', showWeekly and weeklySupported, showWeeklyOpen, showWeeklyRetrace, showWeeklyExtend, showWeeklyRange, wOpenPrev, wHighPrev, wLowPrev, wClosePrev, wOpenNow, wStart, mapWeeklyColor, 2)
    f_map_period('M', showMonthly and monthlySupported, showMonthlyOpen, showMonthlyRetrace, showMonthlyExtend, showMonthlyRange, mOpenPrev, mHighPrev, mLowPrev, mClosePrev, mOpenNow, mStart, mapMonthlyColor, 2)
    f_map_poc('D', calDEnabled and dailySupported, calDPOC, calDFrom, calDStatus, calDDev, dStart, amber, 1)
    f_map_poc('W', calWEnabled and weeklySupported, calWPOC, calWFrom, calWStatus, calWDev, wStart, color.rgb(215, 171, 85), 2)
    f_map_poc('M', calMEnabled and monthlySupported, calMPOC, calMFrom, calMStatus, calMDev, mStart, color.rgb(199, 148, 64), 3)
    if showPOC
        f_map_add(latestPOC, 'SWING POC', 'Swing POC', amber, line.style_solid, 2, latestPOCConfirmed, profileStatus + '. Confirmed signal-derived swing estimate; became available at reversal confirmation.', true, true)

// Immutable scalar snapshot records. No mutable reference objects are retained.
type AtlasSnapshot
	int number
	int stamp
	int direction
	float entry
	float swingPOC
	float dayPOC
	float weekPOC
	float monthPOC
	float above
	float below
	string aboveName
	string belowName
	int supertrend
	float vwap
	string quality
var array<AtlasSnapshot> signalSnapshots = array.new<AtlasSnapshot>()
if showSnapshots and barstate.isconfirmed and (buy or sell)
    float above = na
    float below = na
    string aboveName = '--'
    string belowName = '--'
    for ref in atlasRefs
        isFib = str.contains(ref.family, 'Body') or str.contains(ref.family, 'Range')
        if isFib and ref.stamp <= time_close
            if ref.price > close and (na(above) or ref.price < above)
                above := ref.price
                aboveName := ref.name
                aboveName
            if ref.price < close and (na(below) or ref.price > below)
                below := ref.price
                belowName := ref.name
                belowName
    quality = 'Source at signal: ' + diagSource + '\nSwing: ' + profileStatus + '\nD: ' + calDStatus + '\nW: ' + calWStatus + '\nM: ' + calMStatus + '\nSession: ' + sessStatus
    array.push(signalSnapshots, AtlasSnapshot.new(tradeNumber, time_close, side, close, showPOC ? latestPOC : na, calDPOC, calWPOC, calMPOC, above, below, aboveName, belowName, stDirection, sessVWAP, quality))
    if array.size(signalSnapshots) > snapshotKeep
        array.shift(signalSnapshots)
AtlasSnapshot inspected = na
if array.size(signalSnapshots) > snapshotBack
    inspected := array.get(signalSnapshots, array.size(signalSnapshots) - 1 - snapshotBack)
    inspected
    // These expose the selected snapshot, not a strategy or historical P&L series.
plot(not na(inspected) ? inspected.entry : na, 'Snapshot selected / entry close', display = display.data_window)
plot(not na(inspected) ? inspected.above : na, 'Snapshot selected / Fib above', display = display.data_window)
plot(not na(inspected) ? inspected.below : na, 'Snapshot selected / Fib below', display = display.data_window)

// Seven explicitly named reference streams. First eligible touch per identity.
// Existing pending events continue after their originating level is replaced.
type AtlasTouch
	int stream
	int started
	int expires
	int direction
	float target
	float failure
var array<AtlasTouch> reactionPending = array.new<AtlasTouch>()
var array<string> reactionKeys = array.new_string(7, '')
var array<bool> reactionTouched = array.new_bool(7, false)
var array<int> reactionResults = array.new_int()
var array<int> reactionGroups = array.new_int()
var array<int> reactionBreakdown = array.new_int(70, 0)
// Outcome indices: 0 reaction, 1 failure, 2 ambiguous, 3 expired, 4 invalid.
var array<int> reactionCounts = array.new_int(5, 0)
var int reactionDropped = 0
f_reaction_record(int outcome, int stream, int approach) =>
    cohort = stream * 2 + (approach == 1 ? 0 : 1)
    bucket = cohort * 5 + outcome
    array.set(reactionBreakdown, bucket, array.get(reactionBreakdown, bucket) + 1)
    array.push(reactionGroups, cohort)
    array.push(reactionResults, outcome)
    array.set(reactionCounts, outcome, array.get(reactionCounts, outcome) + 1)
    if array.size(reactionResults) > reactionKeep
        removed = array.shift(reactionResults)
        removedGroup = array.shift(reactionGroups)
        oldBucket = removedGroup * 5 + removed
        array.set(reactionBreakdown, oldBucket, array.get(reactionBreakdown, oldBucket) - 1)
        array.set(reactionCounts, removed, array.get(reactionCounts, removed) - 1)
    0
if showReactions and chart.is_standard and barstate.isconfirmed
    // Resolve from NEXT candle onward. Same-candle target/failure is ambiguous.
    int eventIndex = array.size(reactionPending) - 1
    while eventIndex >= 0
        event = array.get(reactionPending, eventIndex)
        int outcome = -1
        if bar_index > event.started
            if na(high) or na(low) or high < low
                outcome := 4
                outcome
            else
                hitTarget = event.direction == 1 ? high >= event.target : low <= event.target
                hitFailure = event.direction == 1 ? low <= event.failure : high >= event.failure
                outcome := hitTarget and hitFailure ? 2 : hitTarget ? 0 : hitFailure ? 1 : bar_index >= event.expires ? 3 : -1
                outcome
        if outcome >= 0
            f_reaction_record(outcome, event.stream, event.direction)
            array.remove(reactionPending, eventIndex)
        eventIndex := eventIndex - 1
        eventIndex
    levels = array.from(showPOC ? latestPOC : float(na), calDPOC, calWPOC, calMPOC, float(na), float(na), float(na))
    stamps = array.from(latestPOCConfirmed, calDFrom, calWFrom, calMFrom, int(na), int(na), int(na))
    rWBody = wClosePrev - wOpenPrev
    rWRange = wHighPrev - wLowPrev
    rMBody = mClosePrev - mOpenPrev
    rMRange = mHighPrev - mLowPrev
    rWValid = not na(rWBody) and rWRange > 0 and math.abs(rWBody) >= syminfo.mintick and math.abs(rWBody) >= rWRange * minBodyRatio
    rMValid = not na(rMBody) and rMRange > 0 and math.abs(rMBody) >= syminfo.mintick and math.abs(rMBody) >= rMRange * minBodyRatio
    array.set(levels, 4, showDaily and dailySupported and showBodyRetrace and bodyValid ? dClosePrev - 0.618 * dBody : na)
    array.set(levels, 5, showWeekly and weeklySupported and showWeeklyRetrace and rWValid ? wClosePrev - 0.618 * rWBody : na)
    array.set(levels, 6, showMonthly and monthlySupported and showMonthlyRetrace and rMValid ? mClosePrev - 0.618 * rMBody : na)
    array.set(stamps, 4, dStart)
    array.set(stamps, 5, wStart)
    array.set(stamps, 6, mStart)
    for slot = 0 to 6 by 1
        level = array.get(levels, slot)
        stamp = array.get(stamps, slot)
        identity = na(level) or na(stamp) ? '' : str.tostring(stamp) + ':' + str.tostring(level, format.mintick)
        if identity != array.get(reactionKeys, slot)
            array.set(reactionKeys, slot, identity)
            array.set(reactionTouched, slot, false)
        // time >= availability excludes pre-publication OHLC of that candle.
        eligible = identity != '' and not array.get(reactionTouched, slot) and time >= stamp and not na(atr) and atr > 0 and not na(close[1]) and not na(open) and (close[1] > level and open > level or close[1] < level and open < level)
        touch = eligible and not na(high) and not na(low) and high >= low and low <= level and high >= level
        if touch
            array.set(reactionTouched, slot, true)
            approach = close[1] > level ? 1 : -1
            target = level + approach * math.max(syminfo.mintick, atr * reactionMoveATR)
            failure = level - approach * math.max(syminfo.mintick, atr * reactionFailATR)
            // At most seven new observations/bar, with <=100 evaluation bars.
            if array.size(reactionPending) < 707
                array.push(reactionPending, AtlasTouch.new(slot, bar_index, bar_index + reactionBars, approach, target, failure))
            else
                reactionDropped := reactionDropped + 1
                reactionDropped


reactionViewSlot = reactionSourceView == 'Swing POC' ? 0 : reactionSourceView == 'D POC' ? 1 : reactionSourceView == 'W POC' ? 2 : reactionSourceView == 'M POC' ? 3 : reactionSourceView == 'D RET .618' ? 4 : reactionSourceView == 'W RET .618' ? 5 : reactionSourceView == 'M RET .618' ? 6 : -1
f_reaction_matches(int stream, int direction) =>
    (reactionViewSlot == -1 or reactionViewSlot == stream) and (reactionApproachView == 'Both' or (reactionApproachView == 'From above' ? direction == 1 : direction == -1))
selectedReactionCounts = array.new_int(5, 0)
int selectedPending = 0
if barstate.islast
    for stream = 0 to 6 by 1
        for approachSlot = 0 to 1 by 1
            if f_reaction_matches(stream, approachSlot == 0 ? 1 : -1)
                for outcome = 0 to 4 by 1
                    bucket = (stream * 2 + approachSlot) * 5 + outcome
                    array.set(selectedReactionCounts, outcome, array.get(selectedReactionCounts, outcome) + array.get(reactionBreakdown, bucket))
    for event in reactionPending
        if f_reaction_matches(event.stream, event.direction)
            selectedPending := selectedPending + 1
            selectedPending

f_poc_age(int available) =>
    clock = barstate.isrealtime ? timenow : time_close
    minutes = na(available) ? int(na) : int(math.max(0, clock - available) / 60000)
    na(minutes) ? '--' : minutes >= 1440 ? str.tostring(int(minutes / 1440)) + 'd ' + str.tostring(int(minutes % 1440 / 60)) + 'h' : minutes >= 60 ? str.tostring(int(minutes / 60)) + 'h ' + str.tostring(minutes % 60) + 'm' : str.tostring(minutes) + 'm'

int mapHiddenCount = 0
if barstate.islast
    int mapLineUsed = 0
    int mapTagUsed = 0
    int mapZoneUsed = 0
    n = array.size(atlasRefs)
    if n > 0
        prices = array.new_float()
        distances = array.new_float()
        visible = array.new_bool(n, false)
        for i = 0 to n - 1 by 1
            ref = array.get(atlasRefs, i)
            array.push(prices, ref.price)
            array.push(distances, math.abs(ref.price - mapAnchor))
            array.set(visible, i, ref.pinned or mapShowAll)
            if ref.fixedReference
                if ref.price > mapAnchor and (na(nearestAbovePrice) or ref.price < nearestAbovePrice)
                    nearestAbovePrice := ref.price
                    nearestAboveName := ref.name
                    nearestAboveName
                if ref.price < mapAnchor and (na(nearestBelowPrice) or ref.price > nearestBelowPrice)
                    nearestBelowPrice := ref.price
                    nearestBelowName := ref.name
                    nearestBelowName
        // Select two distinct prices on each side, retaining every coincident
        // source name. Equal-within-half-tick levels do not consume both slots.
        byDistance = array.sort_indices(distances, order.ascending)
        selectedAbove = array.new_float()
        selectedBelow = array.new_float()
        for idx in byDistance
            ref = array.get(atlasRefs, idx)
            if not ref.pinned
                selected = ref.price >= mapAnchor ? selectedAbove : selectedBelow
                coincident = false
                for selectedPrice in selected
                    if math.abs(ref.price - selectedPrice) <= syminfo.mintick * 0.5
                        coincident := true
                        coincident
                if coincident or array.size(selected) < 2
                    array.set(visible, idx, true)
                    if not coincident
                        array.push(selected, ref.price)
        // Culling is visual only. Off-focus references still participate in
        // nearest-reference calculation; the dashboard reports hidden count.
        for i = 0 to n - 1 by 1
            ref = array.get(atlasRefs, i)
            if ref.family != 'Swing POC' and not f_map_in_focus(ref.price)
                if array.get(visible, i)
                    mapHiddenCount := mapHiddenCount + 1
                    mapHiddenCount
                array.set(visible, i, false)
        // All individual lines remain at their exact prices, even if tags merge.
        for i = 0 to n - 1 by 1
            ref = array.get(atlasRefs, i)
            if array.get(visible, i) and ref.family != 'Swing POC'
                f_map_line(mapLineUsed, ref)
                mapLineUsed := mapLineUsed + 1
                mapLineUsed
        byPrice = array.sort_indices(prices, order.ascending)
        mergeDistance = math.max(syminfo.mintick * 2, nz(dATRPrev, 0) * mapMergeATR)
        grouped = array.new_bool(n, false)
        if mapLabelsVisible
            for idx in byPrice
                if array.get(visible, idx) and not array.get(grouped, idx)
                    anchor = array.get(atlasRefs, idx)
                    names = anchor.name
                    tip = anchor.name + ' = ' + f_price(anchor.price) + '\n' + anchor.tip
                    count = 1
                    array.set(grouped, idx, true)
                    for j in byPrice
                        item = array.get(atlasRefs, j)
                        if j != idx and array.get(visible, j) and not array.get(grouped, j) and item.price >= anchor.price and item.price - anchor.price <= mergeDistance
                            count := count + 1
                            names := count <= 3 ? names + ' / ' + item.name : names
                            tip := tip + '\n\n' + item.name + ' = ' + f_price(item.price) + '\n' + item.tip
                            array.set(grouped, j, true)
                    caption = count == 1 ? names + '  ' + f_price(anchor.price) : names + (count > 3 ? ' +' + str.tostring(count - 3) : '') + '  [' + str.tostring(count) + ']'
                    f_map_tag(mapTagUsed, bar_index + dailyLabelOffset, anchor.price, caption, anchor.tint, 8, tip + '\n\nDistance from selection anchor: ' + f_map_distance(anchor.price) + '\nGrouped label anchor is the lowest member; individual exact prices above. Analytical references, not orders or probabilities.')
                    mapTagUsed := mapTagUsed + 1
                    mapTagUsed
        // Bounded-span clusters: all members within half tolerance of a seed.
        // This prevents a chain of nearby levels forming an enormous zone.
        if showConfluence and mapMaxZones > 0 and not na(dATRPrev)
            tolerance = math.max(syminfo.mintick * 2, dATRPrev * confluenceATR)
            assigned = array.new_bool(n, false)
            zones = 0
            for idx in byDistance
                seed = array.get(atlasRefs, idx)
                if zones < mapMaxZones and seed.fixedReference and f_map_in_focus(seed.price) and not array.get(assigned, idx)
                    families = array.new_string()
                    members = array.new_int()
                    float zoneLow = seed.price
                    float zoneHigh = seed.price
                    int availableFrom = seed.stamp
                    string details = ''
                    for j = 0 to n - 1 by 1
                        item = array.get(atlasRefs, j)
                        if item.fixedReference and not array.get(assigned, j) and math.abs(item.price - seed.price) <= tolerance * 0.5
                            array.push(members, j)
                            if not array.includes(families, item.family)
                                array.push(families, item.family)
                            zoneLow := math.min(zoneLow, item.price)
                            zoneHigh := math.max(zoneHigh, item.price)
                            availableFrom := math.max(availableFrom, item.stamp)
                            details := details + item.name + ' = ' + f_price(item.price) + '\n'
                            details
                    if array.size(families) >= 2
                        zones := zones + 1
                        for j in members
                            array.set(assigned, j, true)
                        description = array.join(families, ' + ')
                        if zones == 1
                            confluenceSummary := description
                            confluenceSummary
                        pad = math.max(syminfo.mintick, tolerance * 0.05)
                        f_map_zone(mapZoneUsed, availableFrom, zoneHigh + pad, zoneLow - pad)
                        mapZoneUsed := mapZoneUsed + 1
                        if mapLabelsVisible
                            f_map_tag(mapTagUsed, bar_index + dailyLabelOffset + 18, (zoneLow + zoneHigh) / 2, 'CLUSTER ' + str.tostring(array.size(families)), amber, 25, description + '\n' + details + '\nDistinct source families, not independent evidence or probability. Body ratios share one family; current opens/developing POCs excluded. All constituent references were available from this zone\'s left edge.')
                            mapTagUsed := mapTagUsed + 1
                            mapTagUsed

    // Delete only surplus IDs, never the entire map on every realtime tick.
    while array.size(atlasMapLines) > mapLineUsed
        line.delete(array.pop(atlasMapLines))
    while array.size(atlasMapTags) > mapTagUsed
        label.delete(array.pop(atlasMapTags))
    while array.size(atlasMapZones) > mapZoneUsed
        box.delete(array.pop(atlasMapZones))

// Compact cards. Trading metrics stay in tooltips to reduce chart obstruction.
f_pct(_value) =>
    na(_value) ? '--' : str.tostring(_value, '#.##') + '%'
f_row(_table, _row, _name, _value, _color, _tip) =>
    table.cell(_table, 0, _row, _name, text_color = textMuted, text_size = size.small, text_halign = text.align_left, tooltip = _tip)
    table.cell(_table, 1, _row, _value, text_color = _color, text_size = size.small, text_halign = text.align_right, tooltip = _tip)
sample = array.size(netResults)
cardAnchor = cardPosition == 'Top left' ? position.top_left : cardPosition == 'Bottom left' ? position.bottom_left : cardPosition == 'Bottom right' ? position.bottom_right : position.top_right
var table panel = table.new(cardAnchor, 2, 16, bgcolor = panelBg, frame_color = panelHeader, frame_width = 1, border_width = 0)
if barstate.islast
    if fullPanel
        table.cell(panel, 0, 0, 'SWING ATLAS / 369', text_color = textMain, text_size = size.small, text_halign = text.align_left, bgcolor = panelHeader)
        table.cell(panel, 1, 0, timeframe.period + '  |  DEMO', text_color = textMuted, text_size = size.small, text_halign = text.align_right, bgcolor = panelHeader)
        f_row(panel, 1, 'SIGNAL', side == 1 ? 'LONG' : side == -1 ? 'SHORT' : 'WARMUP', trendColor, 'Candle-close confirmation. Signal direction is not a broker position.')
        f_row(panel, 2, 'ENTRY / REVERSE', f_price(entryPrice) + ' / ' + f_price(activeThreshold), textMain, 'Entry close / reversal threshold. Latest swing delay: ' + (na(lastDelay) ? '--' : str.tostring(lastDelay) + ' bars') + '. Active high / low: ' + f_price(tradeHigh) + ' / ' + f_price(tradeLow))
        f_row(panel, 3, 'MARKET', not ready ? 'WARMUP' : warning ? 'REVERSAL WATCH' : isChop ? 'SIDEWAYS' : 'DIRECTIONAL', warning or isChop ? amber : textMain, 'Provisional warnings never execute orders. Sideways classification is informational.')
        f_row(panel, 4, 'COST ESTIMATE', costsSet ? f_pct(roundTripCost) + ' / ROUND TRIP' : 'NOT CONFIGURED', costsSet ? textMuted : amber, 'Commission + spread + slippage. Configure group 02.')
        f_row(panel, 5, 'EXECUTION', 'UNVERIFIED', textMuted, 'Pine cannot receive broker-fill acknowledgments. Check your bridge log. Signal-confirmation prices are not actual fills.')
        f_row(panel, 6, 'SWING POC', showPOC ? f_price(latestPOC) + ' | ' + f_poc_age(latestPOCConfirmed) : 'OFF', amber, profileStatus + '\nAge since POC became available, not since its historical extreme. A retained POC is not the newest swing profile.')
        f_row(panel, 7, 'CALENDAR POC', 'D ' + (na(calDPOC) ? '--' : 'OK') + ' / W ' + (na(calWPOC) ? '--' : 'OK') + ' / M ' + (na(calMPOC) ? '--' : 'OK'), textMuted, 'Source: ' + calSource + ' minutes.\nD: ' + calDStatus + '\nW: ' + calWStatus + '\nM: ' + calMStatus + '\nCompleted POCs arrive after the first source candle of the new period. Missing values are never invented.')
        f_row(panel, 8, 'SUPERTREND', stDirection == 0 or side == 0 ? 'WARMUP' : stDirection == side ? 'ALIGNED' : 'MIXED', stDirection == side ? textMain : amber, 'Closed-bar built-in Supertrend: ' + (stDirection == 1 ? 'BULLISH' : stDirection == -1 ? 'BEARISH' : 'WARMUP') + '. Context only; does not veto or trigger reversals. Line toggle affects visuals only.')
        f_row(panel, 9, 'ABOVE / BELOW', f_price(nearestAbovePrice) + ' / ' + f_price(nearestBelowPrice), textMain, 'Selection anchor: ' + mapSelection + ' = ' + f_price(mapAnchor) + '\nAbove: ' + nearestAboveName + ' | distance ' + f_map_distance(nearestAbovePrice) + '\nBelow: ' + nearestBelowName + ' | distance ' + f_map_distance(nearestBelowPrice) + '\nIncludes references hidden by the visual focus radius. Distances are scenarios, not take-profit orders.')
        f_row(panel, 10, 'REFERENCE MAP', mapShowAll ? 'ALL LEVELS' : '2 + 2' + (mapHiddenCount > 0 ? ' | ' + str.tostring(mapHiddenCount) + ' HIDDEN' : ''), textMuted, 'D blue / W teal / M violet. Gold POCs; dashed opens. Focus hides distant calendar references; latest swing POC stays visible. Show all bypasses focus. Hidden count refers to otherwise-selected map references, not cluster zones. Hover tags for exact prices and distances. Selection: ' + mapSelection)
        f_row(panel, 11, 'CONFLUENCE', showConfluence ? confluenceSummary == 'NO DISPLAYED CLUSTER' ? 'NONE DISPLAYED' : 'CLUSTER PRESENT' : 'OFF', textMuted, confluenceSummary + '\nCounts distinct source families, not probabilities or independent evidence. Only enabled references participate; the focus radius limits displayed clusters.')
    if showPanel and viewPreset == 'Minimal'
        f_row(panel, 0, 'ATLAS / ' + timeframe.period, side == 1 ? 'LONG' : side == -1 ? 'SHORT' : 'WARMUP', trendColor, 'Minimal presentation; all underlying calculations and optional studies continue.')
        f_row(panel, 1, 'ENTRY / REVERSE', f_price(entryPrice) + ' / ' + f_price(activeThreshold), textMain, 'Confirmed signal entry / effective reversal threshold. Not broker fills or resting orders.')
        f_row(panel, 2, 'MARKET', warning ? 'WATCH' : isChop ? 'SIDEWAYS' : 'DIRECTIONAL', warning or isChop ? amber : textMuted, 'Visual context only. This does not block or create reversal signals.')
        if showPOC
            f_row(panel, 3, str.contains(profileStatus, 'Prior valid POC retained') ? 'POC RETAINED' : 'POC / AGE', f_price(latestPOC) + ' / ' + f_poc_age(latestPOCConfirmed), amber, profileStatus + '\nAge since availability; not a market-strength measure.')
    if metricsVisible
        baseRow = showPanel ? 12 : 0
        avgNet = sample > 0 ? array.avg(netResults) : na
        winRate = sample > 0 ? wins * 100.0 / sample : na
        pfText = sample == 0 ? '--' : losingSum > 0.00000001 ? str.tostring(winningSum / losingSum, '#.##') : winningSum > 0 ? 'NO LOSSES' : '--'
        table.cell(panel, 0, baseRow, 'MODELED METRICS', text_color = textMain, text_size = size.tiny, text_halign = text.align_left, bgcolor = panelHeader)
        table.cell(panel, 1, baseRow, str.tostring(sample) + ' TRADES', text_color = textMuted, text_size = size.tiny, text_halign = text.align_right, bgcolor = panelHeader, tooltip = 'Rolling sample. Total completed: ' + str.tostring(totalCompleted) + '. Modeled close-to-close returns; no broker fills, funding, leverage or liquidation model.')
        f_row(panel, baseRow + 1, 'WIN RATE / PF', f_pct(winRate) + ' / ' + pfText, textMain, 'PF = sum of positive net percentage returns / absolute sum of negative net percentage returns. Open trade excluded.')
        f_row(panel, baseRow + 2, 'AVG NET / LAST', f_pct(avgNet) + ' / ' + f_pct(lastNet), na(avgNet) ? textMuted : avgNet >= 0 ? upColor : dnColor, costsSet ? 'After configured estimated costs; not actual execution results.' : 'Costs are zero. Configure group 02 before interpreting results.')
        f_row(panel, baseRow + 3, 'AVG HOLD', sample > 0 ? str.tostring(array.avg(holdBars), '#.0') + ' BARS' : '--', textMuted, sample > 0 ? 'Elapsed: ' + str.tostring(array.avg(holdMinutes), '#.0') + ' min. Average MFE / MAE: ' + f_pct(array.avg(favorable)) + ' / ' + f_pct(array.avg(adverse)) + '. Worst MAE: ' + f_pct(array.max(adverse)) + '. Entry candle pre-entry wicks excluded.' : 'No completed signal trades yet.')

// Research card uses tiny type and its own corner. Presets affect rendering only.
f_research_row(_table, _row, _name, _value, _tint, _tip) =>
    table.cell(_table, 0, _row, _name, text_color = textMuted, text_size = size.tiny, text_halign = text.align_left, tooltip = _tip)
    table.cell(_table, 1, _row, _value, text_color = _tint, text_size = size.tiny, text_halign = text.align_right, tooltip = _tip)
f_when(int stamp) =>
    na(stamp) ? '--' : str.format_time(stamp, 'MM-dd HH:mm', syminfo.timezone)
researchAnchor = researchPosition == 'Top left' ? position.top_left : researchPosition == 'Top right' ? position.top_right : researchPosition == 'Bottom right' ? position.bottom_right : position.bottom_left
var table researchPanel = table.new(researchAnchor, 2, 19, bgcolor = panelBg, frame_color = panelHeader, frame_width = 1)
if barstate.islast
    table.clear(researchPanel, 0, 0, 1, 18)
    if viewPreset == 'Analysis'
        if showQuality
            f_research_row(researchPanel, 0, 'DATA QUALITY', chart.is_standard ? 'STANDARD CANDLES' : 'NONSTANDARD CHART', textMain, 'Data diagnostics only. Not an accuracy rating. Standard time-based candles are the intended input.')
            f_research_row(researchPanel, 1, 'SWING SOURCE', showPOC ? diagSource : 'OFF', amber, 'Requested lower timeframe: ' + profileTF + '. Last processed closed chart candle: ' + diagSource + '. Stored volume samples: ' + str.tostring(array.size(volumeSamples)) + ' / ' + str.tostring(profileCapacity))
            f_research_row(researchPanel, 2, 'FALLBACK / MISSING', str.tostring(diagFallback) + ' / ' + str.tostring(diagMissing), textMuted, 'Counts among ' + str.tostring(diagBars) + ' sampled chart candles in loaded history. Missing/negative chart volume is counted separately and may also be a fallback candle. Neither count estimates missing exchange ticks.')
            f_research_row(researchPanel, 3, 'D POC AVAILABLE', f_when(na(calDPOC) ? int(na) : calDFrom), textMuted, calDStatus + '\nSource ' + calSource + 'm; timestamp uses exchange timezone. Availability is delayed as documented.')
            f_research_row(researchPanel, 4, 'W POC AVAILABLE', f_when(na(calWPOC) ? int(na) : calWFrom), textMuted, calWStatus + '\nSource ' + calSource + 'm; timestamp uses exchange timezone.')
            f_research_row(researchPanel, 5, 'M POC AVAILABLE', f_when(na(calMPOC) ? int(na) : calMFrom), textMuted, calMStatus + '\nSource ' + calSource + 'm; timestamp uses exchange timezone.')
        if showSnapshots
            f_research_row(researchPanel, 6, 'FROZEN SNAPSHOT', na(inspected) ? 'UNAVAILABLE' : '#' + str.tostring(inspected.number) + ' | ' + f_when(inspected.stamp), textMain, 'Record 0 is newest; change Inspect snapshot to browse retained signals. Snapshots copy confirmation-close information and are reconstructed on reload.')
            if not na(inspected)
                f_research_row(researchPanel, 7, inspected.direction == 1 ? 'BUY CLOSE' : 'SELL CLOSE', f_price(inspected.entry), inspected.direction == 1 ? upColor : dnColor, inspected.quality)
                f_research_row(researchPanel, 8, 'SWING / D POC', f_price(inspected.swingPOC) + ' / ' + f_price(inspected.dayPOC), amber, 'Frozen W POC: ' + f_price(inspected.weekPOC) + '\nFrozen M POC: ' + f_price(inspected.monthPOC) + '\nThese were available at this signal close; never predicted outcomes.')
                f_research_row(researchPanel, 9, 'FIB ABOVE / BELOW', f_price(inspected.above) + ' / ' + f_price(inspected.below), textMuted, 'Above: ' + inspected.aboveName + '\nBelow: ' + inspected.belowName + '\nNearest enabled BODY/RANGE Fib references relative to the signal close, independent of visual focus/preset.')
                f_research_row(researchPanel, 10, 'ST / SESSION VWAP', (inspected.supertrend == 1 ? 'BULL' : inspected.supertrend == -1 ? 'BEAR' : '--') + ' / ' + f_price(inspected.vwap), textMuted, 'Frozen confirmed Supertrend direction and closed-candle session VWAP; unavailable source data remains blank.')
        if showReactions
            f_research_row(researchPanel, 11, 'FIRST-TOUCH STUDY', chart.is_standard ? str.tostring(array.sum(selectedReactionCounts)) + ' SELECTED' : 'STANDARD CHART ONLY', textMain, 'View: ' + reactionSourceView + ' / ' + reactionApproachView + '. Selected subset of ' + str.tostring(array.size(reactionResults)) + ' globally retained outcomes. Streams: swing POC, D/W/M POC, D/W/M body .618 retracement, when enabled. One first eligible touch per level identity. Overlapping observations are not independent trades. No success probability is claimed.')
            f_research_row(researchPanel, 12, 'REACTION / FAILURE', str.tostring(array.get(selectedReactionCounts, 0)) + ' / ' + str.tostring(array.get(selectedReactionCounts, 1)), textMuted, 'Frozen chart-ATR distances at touch-candle close: reaction ' + str.tostring(reactionMoveATR) + ', failure ' + str.tostring(reactionFailATR) + '. Evaluation begins NEXT candle, ends after ' + str.tostring(reactionBars) + ' candles. First threshold reached resolves; both in one candle are ambiguous.')
            f_research_row(researchPanel, 13, 'AMBIG / EXPIRED / BAD', str.tostring(array.get(selectedReactionCounts, 2)) + ' / ' + str.tostring(array.get(selectedReactionCounts, 3)) + ' / ' + str.tostring(array.get(selectedReactionCounts, 4)), textMuted, 'Ambiguous = both thresholds within one later candle. Expired = neither within the window. Bad = invalid evaluation OHLC. Touch-candle extremes are never used for scoring.')
            f_research_row(researchPanel, 14, 'PENDING', str.tostring(selectedPending), textMuted, 'Pending observations are excluded from resolved counts. Capacity omissions: ' + str.tostring(reactionDropped) + '. No returns, fees, broker fills or confidence percentages are inferred.')
        if showSession
            f_research_row(researchPanel, 15, 'CUSTOM SESSION', sessionHours + ' | ' + sessTZ, textMain, 'Session days: ' + sessionDays + '. Closed chart candles only. Boundaries use named timezone rules, including DST. First loaded and unaligned sessions are skipped.')
            f_research_row(researchPanel, 16, 'OPEN / VWAP', f_price(sessOpenKnown) + ' / ' + f_price(sessVWAP), textMuted, 'Current observed session. VWAP = sum(HLC3*volume)/sum(volume); unavailable when volume or coverage is invalid.')
            f_research_row(researchPanel, 17, 'PREVIOUS HIGH / LOW', f_price(sessPrevHigh) + ' / ' + f_price(sessPrevLow), textMuted, sessPriorStatus + '. Published at the first confirmed chart candle outside that session or in the next session; not backdated.')
            f_research_row(researchPanel, 18, 'SESSION COVERAGE', sessStatus, sessCoverage ? textMuted : amber, 'Requires a bar at the exact opening and a final bar ending at the exact session close. Interior missing feed candles cannot always be distinguished from no trading. No certified tick-completeness claim.')

// One target-position event. A bridge must inspect the actual demo position,
// close the opposite side, confirm the fill, then open the requested side.
// Deduplicate event_id; reconcile after outages. Configure size in the bridge.
// Use ONE timeframe per symbol/account unless a bridge arbitrates conflicts.
if sendAlerts and (buy or sell)
    target = buy ? 'long' : 'short'
    eventId = syminfo.tickerid + '_' + timeframe.period + '_' + str.tostring(time_close)
    plain = buy ? 'BUY | close SHORT then open LONG' : 'SELL | close LONG then open SHORT'
    payload = '{"schema":"jurik_reversal_v1","mode":"demo","action":"set_target_position","target":"' + target + '","symbol":"' + syminfo.tickerid + '","timeframe":"' + timeframe.period + '","event_id":"' + eventId + '","bar_close_ms":' + str.tostring(time_close) + ',"signal_price":' + str.tostring(close, format.mintick) + '}'
    alert(jsonAlerts ? payload : plain, alert.freq_once_per_bar_close)

alertcondition(buy, 'BUY - target LONG', 'BUY: close SHORT then open LONG on {{ticker}} at {{close}}.')
alertcondition(sell, 'SELL - target SHORT', 'SELL: close LONG then open SHORT on {{ticker}} at {{close}}.')
